Capistrano deploys without tears
Capistrano automates deployment over SSH with atomic releases and one-command rollbacks. A practical guide to the releases/current model, shared files, the Rails task chain, multistage config, and the gotchas worth knowing.
Deploying a Rails app by hand — git pull, bundle, migrate, assets:precompile,
restart, and pray — works until the day it doesn’t, and the day it doesn’t is
usually the day you most needed it to. Capistrano replaces that ritual with one
repeatable command that does the same steps in the same order every time, makes the
switch to the new code atomic, and lets you roll back instantly when something is
wrong. For Ruby teams it is the default deploy tool, and once it is set up,
deploying stops being a thing you dread. Here is how it works and how to set it up
without the tears the title promises to spare you.
The mental model: releases and a current symlink
Everything about Capistrano makes sense once you understand its directory layout on
the server. It does not deploy into a single directory and overwrite it. Instead it
keeps a releases/ folder with timestamped directories, one per deploy, and a
current symlink pointing at the active one:
/var/www/myapp/
├── releases/
│ ├── 20150518093000/
│ ├── 20150519140500/
│ └── 20150520101500/ <- newest
├── current -> releases/20150520101500/
├── repo/ <- the git mirror
└── shared/ <- persists across releases
A deploy clones the new code into a fresh timestamped directory, runs all the
build steps there while the old release is still serving traffic, and only at the
very end flips the current symlink to the new directory in one atomic operation.
This is the feature that matters most: the cutover is a single symlink change, so
users never see a half-built release. And rollback is just flipping current back to
the previous timestamp — cap production deploy:rollback — which is instant because
the old release is still sitting right there on disk.
Shared files and directories
The timestamped-release model raises an obvious question: what about things that must
survive a deploy — uploaded files, logs, the master.key, compiled assets you want
to reuse? Those live in shared/, and Capistrano symlinks them into each new release:
# config/deploy.rb
set :linked_files, %w[config/database.yml config/secrets.yml]
set :linked_dirs, %w[log tmp/pids tmp/cache public/system public/uploads]
linked_files and linked_dirs are the mechanism that keeps state out of the
disposable release directories. Your production database.yml and secrets.yml live
once in shared/config/ (placed there out-of-band, never committed), and every
release symlinks to them. User uploads in public/uploads persist because they too
live in shared/. The discipline mirrors what we learned containerising stateful
services: the release is disposable, the shared data is precious, and they have
different lifecycles.
The Rails task chain
Capistrano is a task runner, and its plugins wire the familiar deploy steps into the right place in the release lifecycle:
# Gemfile
gem "capistrano-rails" # assets + migrations
gem "capistrano-bundler" # bundle install
gem "capistrano-rbenv" # the right Ruby
gem "capistrano3-puma" # restart the app server
With those required, a deploy runs, in order: fetch the code into the new release,
bundle install, run pending migrations, precompile assets, then symlink current
and restart the app server. Two niceties worth knowing: capistrano-rails is smart
enough to skip asset precompilation when nothing under app/assets changed (a big
time saver, since precompiling is the slowest step), and migrations run against the
new code before the symlink flips. The whole chain is one command:
cap production deploy
Multistage: staging and production from one config
You almost always deploy to more than one environment, and Capistrano’s multistage
support keeps them honest. Shared settings live in config/deploy.rb; per-environment
specifics live in config/deploy/staging.rb and config/deploy/production.rb:
# config/deploy/production.rb
server "web1.example.com", user: "deploy", roles: %w[app web db]
set :branch, "master"
# config/deploy/staging.rb
server "staging.example.com", user: "deploy", roles: %w[app web db]
set :branch, ENV["BRANCH"] || "develop"
Now cap staging deploy and cap production deploy use the same task chain against
different hosts and branches — which is exactly how you ensure staging genuinely
rehearses production rather than drifting into a different process.
The gotchas worth knowing
A few things that cost people an afternoon the first time:
- First deploy needs the shared files in place. Capistrano symlinks
linked_files, but it does not create them —shared/config/database.ymlmust exist on the server before your first deploy, or it fails. Put them there manually (or with a one-off task) up front. deploy:checkbefore you deploy. It verifies the directory structure, the linked files, and SSH access without deploying. Run it first on a new server and fix what it complains about.- Migrations are not automatically zero-downtime. Capistrano makes the code cutover atomic; it does not make a destructive migration safe. A migration that drops a column the old release is still using will break things during the brief overlap. Backwards-compatible migrations (add columns before you use them, remove them a deploy later) are still your job.
currentsymlink flips last, but the app server must reload it. Make sure your Puma/Unicorn restart actually picks up the newcurrent— a phased restart that keeps old workers pointed at the old release is a classic source of “I deployed but nothing changed”.- Keep a sensible
keep_releases. Old releases pile up on disk;set :keep_releases, 5prunes them while keeping enough history for a rollback.
Verdict
Capistrano turns deployment from an anxious manual ritual into a boring, repeatable
command — which is exactly what you want deployment to be. The releases-and-symlink
model gives you atomic cutovers and instant rollbacks for free, shared/ keeps your
state safe across deploys, the plugin chain handles the Rails specifics, and
multistage keeps staging and production aligned. Set up the shared files once, write
backwards-compatible migrations, and cap production deploy becomes a command you run
without a second thought. In a later era we will deploy with containers and pipelines,
but the ideas Capistrano drills in — atomic releases, separated state, repeatable
steps, easy rollback — are the ones every deploy system after it is still built on.