Better late than never, right? As we get ready to upgrade our servers I thought it’d be a good time to upgrade our deployment process. Currently pushing out a new version of GitHub takes upwards of 15 minutes. Ouch. My goal: one minute deploys (excluding server restart time).
We currently use Capistrano with a 400 line deploy.rb file. Engine Yard provides a handful of useful Cap tasks (in gem form) that we use along with many of the built-in features. We also use the fast_remote_cache deployment strategy and have written a handful (400 lines or so) of our own tasks to manage things like our service hooks or SVN importer.
As you may know, Capistrano keeps a releases directory where it creates timestamped versions of your app. All your daemons and processes then assume your app lives under a directory called current which is actually a symlink to the latest timestamped version of your app in releases. When you deploy a new version of your app, it’s put into a new timestamped directory under releases. After all the heavy lifting is done the current symlink is switched to it.
Which was really great. Before Git. So I went digging.
First I investigated Vlad the Deployer, the Capistrano alternative in Ruby. I like that it’s built on Rake but it seems to make the same assumptions as Capistrano. Basically both of these tools are modular and built in such a way that they work the same whether you’re using Subversion, Perforce, or Git. Which is great if you’re using SVN but unfortunate if you’re using Git.
For example, this is from Vlad’s included Git deployment strategy:
When you deploy a new copy of your app, Vlad removes the existing copy and does a full clone to get a new version. Capistrano does something similar by default but has a bundled “remote_cache” strategy that is a bit smarter: it caches the Git repo and does a fetch then a reset. It still has to then copy the updated version of your app into a timestamped directory and switch the symlink, but it’s able to cut down on time spent pulling redundant objects. It even knows about the depth option.
The next thing I looked at was Heroku’s rush. It lets you drive servers (even clusters of them) using Ruby over SSH, which looked very promising. Maybe I’d write a little git-deploy script based on it.
Unfortunately for me Rush needs to be installed on every server you’re managing. It also needs a running instance of rushd. Which makes sense – it’s a super powerful library – but that wouldn’t work for deploying GitHub.
Fabric is a library I first heard about back in February. It’s like Capistrano or Vlad but with more emphasis on being a framework/tool for remote management of servers. Easy deployment scripts are just a side effect of that mentality.
It’s very powerful and after playing with it for a while I was extremely pleased. I’ll definitely be using it in all my Python projects. However, I wasn’t looking forward to porting all our custom Capistrano tasks to Python. Also, though I love Python, we’re mostly a Ruby shop and everyone needs to be able to add, debug, and modify our deploy scripts with ease.
Playing with Fabric did inspire me, though. Capistrano is basically a tool for remote server management, too, if you think about it. We may have outgrown its ideas about deployment but I can always write my own deployment code using Capistrano’s ssh and clustering capabilities. So I did.
It turned out to be pretty easy. First I created a config/deploy directory and started splitting up the deploy.rb into smaller chunks:
$ ls -1 config/deploy gem_eval.rb import.rb notify.rb queue.rb services.rb settings.rb sudo_everywhere.rb symlinks.rb
Then I pulled them in. Careful here: Capistrano override both load and require so it’s probably best to just use load.
This separation kept the deploy.rb and each specific file small and focused.
Next I thought about how I’d do Git-based deployment. Not too different from Capistrano’s remote_cache, really. Just get rid of all the timestamp directories and have the current directory contain our clone of the Git repo. Do a fetch then reset to deploy. Rollback? No problem.
The best part is that because Engine Yard’s gemified tasks and our own code both call standard Capistrano tasks like deploy and deploy:update, we can just replace them and not change the dependent code.
Here’s what our new deploy.rb looks like. Well, the meat of it at least:
Great. I like this – very Gitty and simple. But copying and removing directories wasn’t the only slow part of our deploy process.
Every Capistrano task you run adds a bit of overhead. I don’t know exactly why, but I imagine each task opens a fresh SSH connection to the necessary servers. Maybe. Either way, the less tasks you run the better.
We were running about eight symlink related tasks during each deploy. Config files and cache directories that only live on the server need to be symlinked into the app’s directory structure after the reset. Cutting these actions down to a single task made everything much, much faster.
Here’s our symlinks.rb:
Finally, bundling CSS and JavaScript. I’d like to move us to Sprockets but we’re not using it yet and this adventure is all about speeding up our existing setup.
Since the early days we’ve been using Uladzislau Latynski’s jsmin.rb to minimize our JavaScript. Our Cap task looked something like this:
Spot the problem? We’re minimizing the JS locally, on every deploy, then uploading it to each server individually. We also do this same process for Gist’s JavaScript and the CSS (using YUI’s CSS compressor). So with N servers, this is basically happening 3N times on each deploy. Yowza.
Solution? Do the minimizing and bundling on the servers. The beefy, beefy servers:
As long as the bundle Rake tasks don’t need to load the Rails environment (which ours don’t), this is much faster.
Conclusion
We moved to a more Git-like deployment setup, cut down the number of tasks we run, and moved bundling and minimizing JS and CSS from our localhost to the server. Did it help?
As I said before, a GitHub deploy can take 15 minutes (not counting server restarts). My goal was to drop it down to 1 minute. How’d we do?
$ time cap production deploy
* executing `production'
* executing `deploy'
triggering before callbacks for `deploy:update'
* executing `notify:campfire'
* executing `deploy:update'
* executing `deploy:update_code'
triggering after callbacks for `deploy:update_code'
* executing `symlinks:make'
* executing `deploy:bundle'
* executing `deploy:restart'
* executing `mongrel:restart'
* executing `deploy:cleanup'
real 0m14.361s
user 0m2.049s
sys 0m0.560s
15 minutes down to 14 seconds. Not bad.






Pretty sweet. Thanks for sharing.
Using a Git based deploy is definitely the right thing to do.
Opening a new SSH connection each time is going to slow down stuff, but you can have SSH use an existing connecting for subsequent connects:
Nice post.
Thanks for sharing +1
Yeah, moving the release versions inside git is definitely the way to go. One variation I've seen still keeps the capistrano timestamp approach as local branch names. This keeps everything in sync with any deploy log you maintain as well as making it easier to revert if necessary after any number of successful deploys.
Been doing similar stuff with vlad for a long time. Rejected capistrano early on because I don't want to learn a bunch of cruft so that I can write my own scripts that just execute a bunch of commands over ssh.
Thanks for the post, this is a great message for people who were worried about scaling with Capistrano, I can't think of using anything else, and the idea of Vlad turns my stomach, great to note you took a couple wildcard options into consideration!
I may be misunderstanding something, but in the rollback task it checks out HEAD^, which is just the parent commit. Would it be better to use HEAD@{1}, or something similar, to get the last checked-out commit, in case you have multiple commits between deploys?
Nice post, Chris. You might want to take a look at fast_remote_cache as well, it's what we use to solve the same problem: http://github.com/37signals/fast_remote_cache/tree
You hit the nail on the head, Chris, with rolling your own. Too many people see the default deployment tasks as the entirety of cap, and despair when the default tasks fail them. It really is not hard to write your own deployment tasks. Thank-you for writing this up!
Chris,
I've had worked in the same problem in our system and reduced the time a lot too. Basicaly we don't delete anything during the deploy(just move), only in a cron job every night. Our deploy is very fast too.
I will implement some of your ideas too.
My understanding is that Capistrano uses the
releases/YYYMMDDHHMMdirectory structure andcurrentsymlink as a way to ensure an atomic switchover during deployment. Does simply runninggit reset --hardatomically switch the release, or are you mixing in an load-balancer recipe that ensures the host doesn't receive any traffic during the deployment?@natacado @git reset --hard@ is very, very fast.
Thanks for the post.
It must not matter for a Rails app but I've found the
-Toption tolnvery useful when writing deploy systems.@natacado: You can always display a maintenance page if you are concerned about limiting traffic to your site while a deployment is going on. It's not the most ideal as the people who visit your site are never happy to see a maintenance page. However if deploys on a larger site like Github only take 14 seconds, then your deploys should be shorter.
Thanks for writing this up. My deployments have been taking longer and longer due to git submodules (N clones each deploy). Your recipes along with a basic
git submodule init && git submodule updateshould speed it up. Thanks.anyone else running into an issue with overriding the rollback task?
"/Library/Ruby/Gems/1.8/gems/capistrano-2.5.8/lib/capistrano/configuration/namespaces.rb:97:in
task': defining a task namedrollback' would shadow an existing namespace with that name (ArgumentError)"Yeah, I was just going to post about the issue I was having with the rollback task. I get the exact same error.
mkrisher/brycethornton, same here. Any solution?
Wow ... 5600% improvement. That's quite impressive.
Hey Chris - check out the dancing_with_sprockets rails plugin when you are ready to move to sprockets. I like Sam's sprockets, but the sprockets-rails plugin caused me pain.
http://github.com/coderifous/dancing_with_sprockets
Hmm, I'm in the same boat with @chrismear. HEAD^ will just rollback to the previous commit. HEAD@{1} seems better. Unless I'm missing something....
Regarding the problem with the "defining a task named 'rollback' would shadow an existing namespace with that name (ArgumentError)" error. This is caused because "rollback" is actually a namespace with a default task. So to prevent the error you should use something like this:
namespace :rollback do
end
And here the code that should actually work...
(the code belongs within the :deploy namespace) Next time I test the code before posting ;)
@mkrisher / @kommen : As you define all the tasks yourself and only use Capistrano as a wrapper for executing commands remotely, you can remove:
from your Capfile.
I forked @defunkt's gist and applied some of the changes suggested above (@kommen & @chrismear) as well as a few of my own improvements. It tries to make things run as smoothly as possible with other cap recipes|plugins that you might be using or might want to use.
I recently wrote about this on my blog. Including a gist of my full deploy.rb that can be dropped into most normal deployments.
There is also Inploy: http://github.com/dcrec1/inploy
If you're using Git and deploying to Passenger, apparently it rocks. I plan on trying it for my next project.
Also, how about removing the spam above this comment?
Here's a cap plugin wrapping up the idea:
http://github.com/smtlaissezfaire/fast_git_deploy