This is why I am not a sysadmin. I just spent the better part of this evening figuring out just how to get my latest app deployed to my production environment with Capistrano. Sure it is in fact easyish deployment, but you have to know the dance of how it all comes together. And there’s a lot of steps. I wrote this post to hopefully pull together a whole bunch of different information rather than have you read many separate links.
So first off, here’s my setup:
The Setup
Local Development
- A Mac laptop w/ Snow Leopard (10.6.4)
- Rails 2.3.8
- Capistrano 2.5.19
- Passenger 2.2.8
Source Repository
Production Server
- Red Hat Enterprise Linux on a virtual server
- Passenger 2.2.15
- Apache
- MySQL
The Plan
- Add Capistrano to the app
- Create a private Github repo for the app
- Create release branch
- Create an app-specific user
- Publish to the production server
Add Capistrano to the App
This first part is fairly easy thanks to:
capify .
If you end up creating the Capfile yourself you should be aware that capify does add a few extra goodies to the file:
# Capfile
load 'deploy' if respond_to?(:namespace) # cap2 differentiator
Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
load 'config/deploy' # remove this line to skip loading any of the default tasks
There wasn’t much I needed to do to the Capfile itself since I don’t have any custom tasks. But there was a bit of setup that needed to happen in the deploy.rb file. One thing to note is that I ended up switching my SSH port to a different one when I set up my server, so that explains the port on the roles:
# deploy.rb
set :application, "coolapp"
set :repository, "git@github.com:MYUSER/MYREPO.git"
set :deploy_to, "/var/www/APPNAME"
set :user, "app_deployer"
set :branch, "release"
set :git_enable_submodules, 1
set :use_sudo, false
set :scm, :git
role :web, "www.coolapp.com:12345"
role :app, "www.coolapp.com:12345"
role :db, "www.coolapp.com:12345", :primary => true
# Passenger stuff
namespace :deploy do
task :start do ; end
task :stop do ; end
task :restart, :roles => :app, :except => { :no_release => true } do
run "#{try_sudo} touch #{File.join(current_path,'tmp','restart.txt')}"
end
end
The :repository is my private Github repo. The :deploy_to points to the directory that will hold this Capistrano-based deployment—there is a trick I had to do on /var/www. I created a new Unix user app_deployer. I also have a release branch that is only used for Production pushes. I have submodules, so :git_enable_submodules is set. Since this is a non-privileged user we can’t use sudo. And finally, on the roles you’ll see that I’m SSHing to the production server www.coolapp.com at port 12345.
Rather than deploy straight from the master branch (which I think is just a horrible idea), I created a branch called release. To sync master to release I wrote a quick script:
#!/bin/bash
#
# script/sync_master_to_release.sh
git checkout release
git merge master
git checkout master
Github
I’ve started using Github a lot more. While I’m a big fan of having an in-house server to hold my oh-so-precious source code, I just can’t be bothered to admin it or set one up. Plus I want to be able to connect with other collaborators. So Github seems to be the best way right now to do so.
I set up a paid account so I could get some private repositories. Just a couple clicks of the mouse later and I had a repo set up for this project. The setup instructions are really clear. Since I had a local Git repo for this project on my laptop, all I had to do was add Github as a remote:
git remote add origin git@github.com:MYUSER/MYREPO.git
(I did in fact have an origin already set up, but I just had to remove the config info in .git/config before running the above line.)
Doing a git push origin master loaded up the Github repo in a flash. And to get the release branch up there it’s just a case of doing a git push origin release to make it aware there’s another branch. Subsequent git push-es pushed up all the objects for both branches.
At this point what I had was an app that was ready to be released to the world and I needed Capistrano to do its deployment magic. But I didn’t just want to grant full access to the deploy script. I don’t want the chance that someone could add tasks that could walk all around the filesystem if they got a hold of the SSH keys…
A User for Deployment
The great thing about Unix/Linux is you have all these permissions so you can lock down directories. The annoying thing about Unix/Linux is that you have to configure all of these permissions.
I SSHed into my production environment as my root user via a plain ol’ terminal and created a new user in a new group:
sudo /usr/sbin/useradd app_deployer
sudo /usr/bin/groupadd deployments
(There was also a step to update the password for app_deployer.)
Then I had to associate the new user and the web server user with the newly-created group in the /etc/group file:
deployments:550:app_deployer,apache
I’m deploying to
/var/www/APPNAME
but when you run Capistrano for the first time it wants to create certain directories. Just for a few minutes I changed the main web directory to world-write permissions:
sudo chmod 777 /var/www
Finally, I could then run Capistrano for the first time:
cap deploy:setup
(Note: Remember that even though I SSHed in via a terminal as a root user, Capistrano’s config file has been set to log in via the “app_deployer” user.)
Once the directories were set up I could twiddle the permissions back to normal locked-down mode:
sudo chmod 755 /var/www
sudo chown -R app_deployer /var/www/APPNAME
sudo chgrp -R deployments /var/www/APPNAME
sudo chmod -R g+s /var/www/APPNAME
This sets the /var/www directory back to world-accessible but NOT world-writable, ensuring that just the app_deployer user can make all the changes it wants to the APPNAME directory. (The “g+s” group-sticky-bit ensures that any new directories created under APPNAME carry the group owner too.)
Now, we’re getting close to deploying, but one thing we need to do is grant our production environment a way to clone our private repo. I logged out of the production terminal session and logged back in instead as the new user:
ssh -p 12345 app_deployer@www.coolapp.com
Then it was a matter of generating some RSA keys:
mkdir .ssh
cd .ssh
ssh-keygen -t rsa
After this I had a newly-minted id_rsa.pub. I turned to my web browser, went to Github, clicked on the Admin link on my app repo, then clicked Deploy Keys. There I created a new key and put up the contents of that public key.
Perfect. Now I can really do the deployment via Capistrano:
cap deploy
At this point messages scrolled by as the release branch was cloned from Github onto the production server into Capistrano-versioned directories. I did run into a couple of errors at first, but that was just because I had some issues with Git submodules. But I think this situation was more unique to me and it turned out in the end I had to juggle some RSA public keys to get things working.
Going Live With Apache+Passenger
(*One thing I didn’t mention was that I had actually done a previous deployment manually where I had SFTPed up my entire codebase to a temp directory and had done the database migrations already. Because of this I didn’t need to ask Capistrano to set up my database too—creating a user for this app, creating a MySQL catalog for the data, initializing the schema. It was also the case I got Passenger working with the test deployment.)
Setting up Passenger is really easy and there’s tons of guides saying how to do it on Linux. The essence of it is:
- Make your Apache server is set up to handle its virtual domains (if you’re using them)
- Make sure your Rails and Rubygems are all working first
- Get the Passenger gem and follow all the instructions (which are really good, btw)
- Put up a test app just to make sure everything’s working OK
Following the Passenger setup instructions there were a few lines that needed to be appended to the httpd.conf file. Instead I created a separate config file in the conf.d directory:
# passenger.conf
LoadModule passenger_module /usr/lib64/ruby/gems/1.8/gems/passenger-2.2.15/ext/apache2/mod_passenger.so
PassengerRoot /usr/lib64/ruby/gems/1.8/gems/passenger-2.2.15
PassengerRuby /usr/bin/ruby
PassengerMaxPoolSize 5
RailsEnv production
That config limits Passenger to 5 instances and ensures that the app is in Production mode. Next I added a config file for the new app:
# coolapp.conf
<VirtualHost *:80>
ServerAdmin contact@coolapp.com
ServerName coolapp.com
ServerAlias www.coolapp.com
DocumentRoot /var/www/coolapp/current/public
<Directory /var/www/coolapp/current/public>
AllowOverride all
Options -MultiViews FollowSymLinks
Allow from All
</Directory>
</VirtualHost>
After a quick Apache restart the app came online w/out any problems, and now whenever I want to release to production all I have to do from my laptop is:
script/sync_master_to_release.sh
git push
cap deploy