Skip to content
← All posts
5 min read Dawid Skłodowski

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.

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.yml must exist on the server before your first deploy, or it fails. Put them there manually (or with a one-off task) up front.
  • deploy:check before 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.
  • current symlink flips last, but the app server must reload it. Make sure your Puma/Unicorn restart actually picks up the new current — 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, 5 prunes 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.