by Jonathan Rochkind, http://bibwild.wordpress.com
Capistrano automates pushing out a new version of your application to a deployment location.
I've been writing and deploying Rails apps for a while, but I avoided using Capistrano until recently. I've got a pretty simple one-host deployment, and even though everyone said Capistrano was great, every time I tried to get started I just got snowed under not being able to figure out exactly what I wanted to do, and figured I wasn't having that much trouble doing it "manually".
But first bundler, and then especially the Rails asset pipeline, finally pushed me over the edge. There's just a bit too many steps now to be happy doing it manually, and Capistrano does some other useful stuff too.
So I figured out how to do Capistrano. I still found the existing documentation and getting started guides not as good as they could be, for me personally at least. I think this is caused by a combination of two things:
- Cap is super flexible in terms of what you're pushing out, and how -- that most guides stay too high level and flexible, but these guides leaves people (or at least me) who want to deploy a Rails app, or even another Rack app, in a conventional standard way... with too many choices they don't want to make and things they have to learn.
- and Cap changing from 1.x to 2.x and changing to keep up with Rails, and when you google, a newbie finds it hard to tell if they're finding something up to date or not.
So here's my attempt to turn what I just learned into a guide for others. I just figured this stuff out, if you know something better, please share in comments!
My situation is: Rails, bundler, asset pipeline, apache, passenger, single host deployment (on a standard self-managed linux box, no heroku or what have you), git. Oh, and a bit of rbenv. I'm going to lead you through that, although it may be useful to you if some things differ, and I'll try to point out other paths where appropriate. I'll also try to explain what's going on and tell you where to go for more info, not just give you boilerplate copy-and-paste, where it makes sense.
I wrote the guide I'd have wanted, it might be too verbose for you, but I like to know what's happening.
This is written in March 2012, using capistrano 2.11.12. If you're reading this far in the future, maybe or maybe not this is still good, or maybe cap, rails, bundler, etc, have changed enough that it ain't.
For reasons I can't tell, some people don't add capistrano to their Gemfiles. So you can just:
gem install capistrano
But I prefer actually adding it to my Gemfile, as:
gem "capistrano", :group => :development
and
bundle install
Now, a command has been installed to actually put a skeleton of cap config files in your project. From your project directory:
capify .
Adds a ./Capfile
and a ./config/deploy.rb
(Note: there's actually often no reason you'd need to keep the Capfile and deploy.rb with the complete source of your project. It could be somewhere else entirely -- when you deploy, you'll need the Capfile and deploy.rb, but you don't actually normally need the source of your whole project, cap's gonna fetch it from the source control anyway! But most people seem to keep their cap deploy configuration in their main project source -- and 'advanced' use might involve cap tasks that work on the checked out source, to add a git tag or something)
This is actually built into Cap, but not turned on by defualt.
In ./Capfile, uncomment load 'deploy/assets'
to get asset precompile behavior.
Now Cap will precompile your assets for you on the production server every time you deploy.
What's it do exactly? Check the source
Why is this in ./Capfile instead of ./config/deploy.rb? Got me, anyone else know? I think it'll work if you put it in deploy.rb instead. Why is it load
instead of require
? I don't know either.
An appropriate Cap recipe is built into bundler and already available to you in Cap. In your ./config/deploy.rb
, add:
require "bundler/capistrano"
And Cap will run something like a "bundle --deployment" on the production server every time you deploy.
(What's a bundle --deployment
? The bundler docs aren't too helpful, but basically: it insists on installing only exactly the gem versions in your checked in Gemfile.lock
, and it installs gems to your local ./vendor
instead of system gem location)
What's it do exactly? Well, as you can see from the 'require', it actually lives in the bundler source, hopefully because the bundler team keeps it up to date with any changes to bundler. The source actually isn't too illuminating, until you figure out the actual bundle:install task is defined in bundler source here. Or when you actually do a cap deploy
later you can look at the output to see what bundler-related things are happening.
At the bottom of the default deploy.rb, there are some lines to uncomment for passenger, that simply have
cap touch ./tmp/restart.txt
on deploy to trigger a passenger reload. Uncomment em!
# If you are using Passenger mod_rails uncomment this:
# 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
Note that if you tell Cap to install your app to say, /opt/my_app
, the actual current deployment will end up in /opt/my_app/current
-- so in your apache or nginx config files, where you're telling it where to find the rails/rack app, you'd want /opt/my_app/current/public
, etc.
Also note that this provides a basic example of how you can define your own tasks, if you need to. You might not need to for a basic Rails setup, there's enough built-in.
Sometimes a new release has some database migrations that need to be run on the production db. (Disclaimer: Some people say never to run migrations on production, but I think I'm not alone in doing so -- it's got some downsides, likely momentary app downtime, but there's no easy better way to do it, without architecting a lot of I don't know what)
Cap's got built in tasks for Rails migrations: You can run cap deploy:migrate
to only run migrations on your server (the one marked with role :db and :primary => true
) without a new deploy push too, or you can cap deploy:migrations
to deploy+migrations. But this requires you to think about "do I need to run migrations this release?"
If you run migrations when they aren't neccesary, then simply nothing will happen. So why not just run them on every deploy? That's what I'm doing. Add to your deploy.rb:
after 'deploy:update_code', 'deploy:migrate'
That's just taking the deploy:migrate
task that was already built in, but telling Cap to run it after deploy:update_code
, every time.
Warning: there's no automated way (I think?) to automatically undo the migrations. You'd have to figure out what Rails migration number you want to go down to, and rake db:migrate
down to it yourself if you want to rollback. Unless I'm wrong and it already magically does this; it's theoretically possible, it would be an interesting project for someone to write such a task.
Where do you figure out what hookpoints to hook in 'after' or 'before'? This graphic helps. But I just looked at the source for Cap's built in deploy/assets
task, and saw it hooked in after "deploy:update_code"
, and figured the point to precompile assets was the right point to db migrate too.
What do the cap migration tasks actually do exactly? They're defined, as of this writing anyway, in the recipes/deploy.rb source file.
Every time you cap deploy
, you get a new copy of your app on your deploy app server(s) in $DEPLOY_DIR/releases/$TIMESTAMP
You probably don't want to keep those around forever eating up more and more disk space. You can run "cap deploy:cleanup" to delete all but the most recent X releases. (looks like at the time I'm writing this default is 5, although some suggest it used to be 3). But why would you want to run that by hand, if you're using capistrano? Have your deploy task automatically clean up old releases after a deploy
Add to your deploy.rb:
set :keep_releases, 5
after "deploy:update", "deploy:cleanup"
Why doesn't cap do this by default? I dunno, some things on the web say it does, but it doesn't, at least for me and my version of Cap.
I only have one server. In ./config.rb
, replace all the "role" lines with a single:
server "your.server", :app, :web, :db, :primary => true
Note, I do have my db running on a different server. You might think you need a role :db pointing to that server. However, with built-in capistrano tasks, there's only one thing (as far as I can tell) with role :db -- a server with role :db, if and only if it also has :primary => true
is used to run your migrations. If you're like me, you don't actually want cap to try to run the migrations on the db server -- the db server does not allow ssh logins and does not have a ruby/rails stack, it's a db server! The migrations should be run on one of the app servers, in my case the only app server.
This :db thing is confusing. Perhaps advanced cap usage actually wants to do something with your actual db servers, but typical cap only (at least with Rails) uses the :db, :primary => true
one for running migrations, which you probably don't actually want to run on an actual db server.
Cap always deploys from a source control repository. (You don't want to deploy something to production that isn't even in source control, do you? I didn't think so). It can handle just about any scm there is, but I use git.
set :scm, :git
set :repository, "giturl"
By default, this will deploy whatever is at the head of the 'master' branch in git. Want to deploy from a specific branch, say 'production' or 'deploy'?
set :branch, "deploy"
You can use a specific git tag instead of branch there too, it's still set :branch
.
Wanna be able to set the 'branch' on the command line, to mention a specific tag? While still defaulting to 'master' or any other branch you want? In your deploy.rb:
set :branch, fetch(:branch, "master")
Then on your command line:
cap -S branch=branchname deploy
(thanks PizzaPill on stackoverflow )
This git checkout will be happening on your target deploy servers, so will by default be done as whatever user cap is using to login to remote servers, which is a whole different ball of wax (see 'Remote user/permissions' below)
It'll ask you for a password if git
does, or do passwordless git checkout if you have it set up, etc. that's fine if the accounts match.
Or you can tell cap what account to use for the git checkout:
set :scm_user, "whatever"
Or if you're me and have a totally brain dead local git server that requires you to hard-code the auth into the git url anyway, then you already did that:
set :repository, "http://USER:[email protected]"
You can probably do something crazy to get USER:PASS from ENV too, out of scope for this guide because I haven't figured it out yet.
by default, capistrano will put your release on your app server at /u/apps/$app_name
(I'm not actually sure where this convention is from). You can specify it yourself, and you can interpolate the "application" setting:
set :application "i_widget_3000"
set :deploy_to "/opt/#{application}"
Okay, there's no getting around a bit of decision making here, and thinking about how you want to set up permissions and such on your deploy servers.
First, understand how Capistrano works: It will execute commands remotely on your deploy server(s) using ssh. To do this, it has to authenticate to your remote server(s) as a certain account. Then it'll, by default anyway, end up creating the release checkout as owned by that account. And under default passenger setup, that means the app will execute as owned by that account.
By default if you do nothing, cap will try to ssh into your remote servers using the same account name you are logged into on the server you're running the cap deploy
command. It'll prompt you for a password unless you have password-less ssh login setup. If your account names match, this may work fine -- but I don't want the file ownership of the release to be under my own developer account, and I don't want the rails app to be run by passenger under that account. It's just poor organizational practice, and even in my small shop several developers (or my hypothetical eventual replacement) might do a deploy, and I don't want the ownership depending on who happened to do the deploy.
It's unclear to me what standard best practices here are. But here's what I figured out for myself:
- On the remote app server(s), I create an account with the same name as my app, say
i_widget_3000
- If my app
deploy_to
is/opt/i_widget_3000
, I create that directory 'out of band', not through cap, andchown
it toi_widget_3000
account. - the
i_widget_3000
account doesn't needsudo
rights or any other special permissions, and can be further limited by whatever sysadminy magic you like to be very limited privs. All it needs to do is be able to read, write, and execute in/opt/i_widget_3000
-- just what you want in an account that web software is going to be running as. (This is why we created that directory 'out of band' -- cap's going to log in asi_widget_3000
, and won't have permissions to create directories itself under/opt
.) - I set up ssh password-less login from my developer account on whatever machines I'll be running
cap deploy
to the i_widget_3000 account on the target deploy server(s). - Other developers that need to do deploys, they just need to set up password-less login too, they can run the
cap deploy
as is, the release ownership will always bei_widget_3000
.
set :user, "i_widget_3000" # you could even do `set :user, application` here
# cap assumes you want to do things with sudo on remote servers, we don't and in fact
# intentionally can't, no problem:
set :use_sudo, false
There are certainly other ways to set things up. You could set up a capistrano
account that cap will use to login, but then tell cap to chown
the files to the actual account that passenger will run the app as. Then capistrano
acct needs to have privileges to do the chown
somehow, but the actual app account can still be limited priv. You could have cap actually log in as the individual developer account (default), but then chown
. You could even have some fancy thing where cap reads what account to login as from the current ENV, so different developers log in as their own account, but the account names don't have to match on different machines.
But this way seemed the simplest way to do what I needed.
There are other guides on the net to this, but basically on the machine you'll be running cap deploy
on, you want to generate your key pair if you haven't already:
ssh-keygen -t rsa
That'll create a ~/.ssh/rsa_id and ~/.ssh/rsa_id.pub
You want to take the contents of the the rsa_id.pub and add them to the end of a file on the target machine, under the account you'll be pushing to, ~/.ssh/authorized_keys
-- note it's important to copy exactly, not introducing any new newlines etc. You can use the built-in openssl utility to do so, if password login is currently enabled for the target account and you know the password.
ssh-copy-id -i ~/.ssh/id_rsa.pub remote_username@remote_host
if you've never deployed to the server(s) before, on the command line:
cap deploy:setup
Then, for the first and subsequent times you want to push a new deploy release:
cap deploy
Note, why have to think about if you need to deploy:setup
or you've already done it? Why not just make deploy
pre-emptively run a setup
every time? The default deploy:setup
is cheap and idempotent (it just makes some directories), so I don't know why not or why nobody does this. I think you could:
before "deploy", "deploy:setup"
cap -T
on the command line to see list of all available cap tasks.
cap -e taskname
to see some help/description for a specific task.
cap deploy:rollback
can undo a deploy, and re-deploy the next-to-latest deployed release. Run it again to go back yet one more version. If you run out of versions, it will tell you and refuse to do it. Useful when you deploy and immediately realize it was a mistake. (Note: I don't think rollback will take care of going down migrations, if the latest release had migrations. So if you auto-migrate on every deploy, beware).
You can use cap deploy:web:disable
to temporarily replace your app with a static maintenance message, and cap deploy:web:enable
to remove it, IF you put something custom in your apache or nginx conf (to redirect all access to maintenance page if it exists). Run deploy:web:disable, it'll tell you what. Here's a suggestion on how to supply a custom maintenance page instead -- would have to be customized at least slightly for cap 2.x, I haven't tried it yet.
I use rbenv on my deploy server(s), via a somewhat experimental rbenv system-wide install
We want to make sure cap can find the 'right ruby' using rbenv when it logs into the remote deploy servers. No problem!
set :default_environment, {
'RBENV_ROOT' => '/usr/local/rbenv',
'PATH' => "/usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH"
}
We also want to have cap's bundler install use binstubs, that are set to use rbenv too, no problem!
set :bundle_flags, "--deployment --quiet --binstubs --shebang ruby-local-exec"
You could adapt this technique for some other kind of production rbenv install too. rbenv in production doens't work completely braindead smoothly yet.
cap will end up executing under whatever the rbenv 'default' ruby is on the deploy machine(s). You want to tell it to use a specific ruby (and make the binstubs for that ruby too), no problem, just add the appropriate ENV variable for rbenv:
set :default_environment, {
'RBENV_VERSION' => '1.9.3-p0',
#...
}
I much prefer that to the level of indirection of pushing a project-specific .rbenv
file with your project, this seems more transparent and simpler.
This guide licensed CC-BY. Actually, you don't even need to give me attribution, but CC curiously doesn't offer a license like that anymore? Do what you will with it, if you wanna turn it into some official cap docs on the cap wiki, go for it.