Close and Go BackBack to Viget

Building an Environment From Scratch With Capistrano 2

Matt Swasey
Matt Swasey, Web Developer, April 30, 2008 6

I recently attended the Pragmatic Programmers Advanced Rails Studio. Overall, I thought it was great. Even though I've had personal experience in most of the topics covered, it filled in a lot of gaps in my personal knowledge. I came out of the three days feeling more well-rounded as a Rails developer. I also met some cool people.

On the topic of deployment, Chad Fowler offered that Capistrano is really just an automated remote shell. That got me thinking: if you could do anything through Capistrano that you could do on the command line, you could not only automate the deployment of the application, but the construction and configuration of the environment in which that application runs.

I rebuilt my 256 slice at Slicehost with a brand new Ubuntu Hardy 8.04 install to test this out. I wanted to see if I could automate the installing and configuring of everything it would take to run a Rails application through Apache and Passenger. It worked! What follows are the steps I took to make it happen.

Setup SSH and sudo

I started with a blank application, and "capified" it. If you don't have Capistrano installed, install it via RubyGems.

sudo gem install capistrano --no-ri --no-rdoc
rails slicehost
cd slicehost/
capify .

Open up config/deploy.rb and start configuring it as you would for any other Rails application. Just make sure you have the same "default_run_options" line as I do. Here's what mine looks like:

default_run_options[:pty] = true

set :application, "slicehost"
set :repository,  "git://github.com/mig/cthulhu.git"

set :deploy_to, "/home/deploy/#{application}"
set :user, "deploy"

set :scm, :git

role :app, "myslice.com"
role :web, "myslice.com"
role :db,  "myslice.com", :primary => true

I've listed "deploy" as my user. This is important – the only bit of manual configuration we will do is to create this user account.

Now I will ssh into my brand new slice, change my password, and create the "deploy" user:

ssh myslice.com -l root

passwd
adduser deploy

The "deploy" user should get sudo privileges. To make things easier for this example, don't require a password. While this is fine for my DMZ-like slice, you should not leave the NOPASSWD flag on your user after finishing – you have been warned.

visudo

Add this line to the bottom:

deploy ALL=(ALL) NOPASSWD: ALL

One last thing, add your public key to the deploy user's authorized_keys file:

On your local machine:

scp ~/.ssh/id_rsa.pub deploy@myslice.com:
ssh myslice.com -l deploy

On your slice:

mkdir .ssh
cat id_rsa.pub >> .ssh/authorized_keys
rm id_rsa.pub
chmod 600 .ssh/authorized_keys
chmod 700 .ssh

Now on to the fun stuff...

Installing the Basics

Open up config/deploy.rb and create a custom namespace for our tasks:

namespace :slicehost do
 # tasks go here
end

I'm going to use Ubuntu's apt-get to install some of the basic necessities. Put these tasks in our :slicehost namespace. I've included tasks for git and sqlite3 here, you might want something different like subversion and postgres. Look at the attached file at the end for more examples.

desc "Update apt-get sources"
task :update_apt_get do
  sudo "apt-get update"
end

desc "Install Development Tools"
task :install_dev_tools do
  sudo "apt-get install build-essential -y"
end

desc "Install Git"
task :install_git do
  sudo "apt-get install git-core git-svn -y"
end

desc "Install SQLite3"
task :install_sqlite3 do
  sudo "apt-get install sqlite3 libsqlite3-ruby -y"
end

To run any of these, drop to your shell and issue a cap command:

cap slicehost:update_apt_get

To see a list of available cap commands, including our custom ones:

cap -T

Install the Rails Stack

The next example is only a little more complex, the thing you should note is the use of sudo within the command string itself. Include the && between commands if you need a command to run in a directory other than the default.

Let's install Ruby and Rails:

desc "Install Ruby, Gems, and Rails"
task :install_rails_stack do
  [
    "sudo apt-get install ruby ruby1.8-dev irb ri rdoc libopenssl-ruby1.8 -y",
    "mkdir -p src",
    "cd src",
    "wget http://rubyforge.org/frs/download.php/29548/rubygems-1.0.1.tgz",
    "tar xvzf rubygems-1.0.1.tgz",
    "cd rubygems-1.0.1/ && sudo ruby setup.rb",
    "sudo ln -s /usr/bin/gem1.8 /usr/bin/gem",
    "sudo gem install rails --no-ri --no-rdoc"
  ].each {|cmd| run cmd}
end

Just run the new installer task and you should be ready to go:

cap slicehost:install_rails_stack

That was easy! We've got a full Rails stack running on our slice. From here we could go a few different routes. I've been eager to try the new mod_rails Passenger out, so let's set that up!

Apache and Passenger (aka mod_rails)

First, we need to install Apache:

desc "Install Apache"
task :install_apache do
  sudo "apt-get install apache2 apache2.2-common apache2-mpm-prefork 
        apache2-utils libexpat1 apache2-prefork-dev libapr1-dev -y"
end

And now the Passenger install. This part is trickiest, because it requires our input on the remote server. This is where the "default_run_options" setting comes in handy.

desc "Install Passenger"
task :install_passenger do
  run "sudo gem install passenger --no-ri --no-rdoc"
  input = ''
  run "sudo passenger-install-apache2-module" do |ch,stream,out|
    next if out.chomp == input.chomp || out.chomp == ''
    print out
    ch.send_data(input = $stdin.gets) if out =~ /enter/i
  end
end

Here's what's happening: the run command is passed a block, the block is telling the run command to step through all output while printing everything to the screen. Any time the words "Enter" or "ENTER" are encountered in the output, the execution waits for our input. Any input is then redirected back into the Passenger installer running on the remote server.

So, we've finished installing all the software we need (for the moment). All that's needed is to configure Apache to use Passenger, and to set up a virtual host for our application.

Apache Configuration

Here is the Apache configuration, taken directly from the output of the Passenger install:

desc "Configure Passenger"
task :config_passenger do
  passenger_config =<<-EOF
LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-1.0.1/ext/apache2/mod_passenger.so
RailsSpawnServer /usr/lib/ruby/gems/1.8/gems/passenger-1.0.1/bin/passenger-spawn-server
RailsRuby /usr/bin/ruby1.8    
  EOF
  put passenger_config, "src/passenger"
  sudo "mv src/passenger /etc/apache2/conf.d/passenger"
end

desc "Configure VHost"
task :config_vhost do
  vhost_config =<<-EOF
<VirtualHost *:80>
ServerName blog.pggbee.com
DocumentRoot #{deploy_to}/public
</VirtualHost>
  EOF
  put vhost_config, "src/vhost_config"
  sudo "mv src/vhost_config /etc/apache2/sites-available/#{application}"
  sudo "a2ensite #{application}"
end

That looks more complicated that it really is. The trick to these tasks is the "put" method – it takes a string and a remote filename and uploads the contents of the string to the file on the remote server.

This allows us to generate the configurations locally, create them on the remote server, and then use sudo to move them into their proper place.

That's it. Our slice is ready for us to deploy as normal. I am not going to cover that here, as it's been covered elsewhere and in much more depth than I can go into here.

Wrapping Up

Once you've created all your custom tasks and verify that they work, it's a good idea to put them all together in one setup method (I use :setup_env in my :slicehost namespace). All I have to do is run the task, sit back, and watch:

cap slicehost:setup_env

Try it out for yourself. Download the entire deploy.rb.txt file, remove the .txt extension and drop it into your project.

Markus said on 05/16 at 09:37 PM

Hi Matt - thanks for the great post! This looks like a very cool way to configure a server - I have one question and excuse my ignorance (Linux newbie): Why do you need to create a “deploy” user at first (as opposed to just using the root account)? Is that actually necessary for running setup_env, or only needed later for regular deployment? Thanks!

Matt Swasey said on 05/19 at 03:49 PM

Hi Markus, thanks for the comment.  There are many different security risks that you are exposing yourself to using the root account for general account activity.  Using something like sudo will give the admin the ability to pick and choose what users get what privileges.  Another thing you will realize if you dive further into Linux/Unix circles, using root for general account activity has been a “no no” for many years, and is always frowned upon.

So while using the root account would certainly work in this case, using it would be doing so against the advisement of decades of Unix nerds.

There are some excellent articles on general system setup and maintenance here: http://articles.slicehost.com, these will help, even if you don’t have a “slice” at slicehost.

Josh Adams said on 05/21 at 10:21 AM

Hey, on this setup:

What does your spin script look like?

Shai Shefer said on 06/19 at 10:18 AM

Matt -

Thanks for this post… really valuable and a great way to minimize slice setup using Capistrano.

Josh Adams said on 06/19 at 10:26 AM

Matt,

On equestion - do you know how to get mysql to install via capistrano?  It always uses curses when I try and so I can’t /completely/ automate our slicebuild.  I have to manually install mysql at present.

-Josh

grantmichaels said on 06/20 at 09:34 PM

Matt,

I can’t thank you enough for this blog post ... The amount of time you have saved me is immeasurable!  Also, for the sake of sharing my experience with linode as opposed to SliceHost, one has to apt-get wget before the script works at the point where it goes out to get rubygems ... It was an easy fix, but I thought I would mention it.  For the sake of clarity, I was installing to a Ubuntu 8.04 Hardy AMD64 on my linode.com account - which apparently doesn’t come w/ wget installed when you drop their image onto your host.  Thanks again for this contribution ...

grantmichaels

Name:

Email:

URL:

Not a robot? Prove it by entering the word below.


Remember my personal information

Notify me of follow-up comments?

A Development Community for Viget Labs and Beyond

Every team member here at Viget Labs strives to be an innovator. We members of the development team are no different - that's why we're constantly engaging in community discussions and exploring the unknown that is the next generation of open-source web applications.

Viget Is Hiring!

Viget has job openings for Ruby Developers, Interns, and Front-End Developers. Learn More »

Recent Comments

Smashing! Thanks for outlining what’s needed to be done so precisely :-)