We recently started receiving support requests about Capistrano 3. Of course to provide quality support you have to know the subject, so I set on a quest to upgrade Semaphore’s deployment script from Capistrano 2 to Capistrano 3. As always it took a bit than expected but in the end new code looks nicer.

I have to say that I did have a flashback from couple of years ago when I was setting up Capistrano for the first time: documentation is missing some things and it’s a bit scattered between the readme, wiki and official homepage. But it’s open source so we are all welcome to contribute improvements.

I will try to make the upgrade easier for you by presenting our old vs new Capistrano configuration step by step.

Bootstrap new configuration

Gemfile

As the first step you have to install new gems. Capistrano 2 didn’t have support for multistage configurations so you had to also use capistrano-ext gem. Capistrano 3 has multistage setup included. It is framework agnostic so you would have to use capistrano-rails gem which adds support for deploying Rails applications. Just update your Gemfile like in the example below, run bundle install and you are ready to start the upgrade process.

Capistrano 2:

group :development do
  gem "capistrano"
  gem "capistrano-ext"
end

Capistrano 3:

group :development do
  gem "capistrano-rails"
end

Capify project with Capistrano 3

As official upgrade guide advises, it’s a good idea to just move old Capistrano files to a safe place and than manually move configuration to newly generated files. Here is a tip how to do that:

mkdir old_cap
mv Capfile old_cap
mv config/deploy.rb old_cap
mv config/deploy/ old_cap

After that you are ready to capify your project with new Capistrano:

bundle exec cap install

Capfile

Among other newly generated files you should also have the new Capfile. Below is how our Capfile used to look like and then the new one.

Capistrano 2:

load "deploy"
load "deploy/assets"
Dir["vendor/gems/*/recipes/*.rb","vendor/plugins/*/recipes/*.rb"].each { |plugin| load(plugin) }
load "config/deploy"

Capistrano 3:

require "capistrano/setup"
require "capistrano/deploy"

require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
require "whenever/capistrano"

Dir.glob("lib/capistrano/tasks/*.cap").each { |r| import r }

Your new Capfile will also contain two commented lines for rvm and rbenv support. We don’t use any of those tools for managing Ruby versions on our servers so I can’t say much much about that part.

require "capistrano/rvm"
require "capistrano/rbenv"

Multistage configuration

Configuration for stages really hasn’t changed much as you can see below. However there is one thing that you need to pay special attention to. The way of telling Capistrano to deploy specific revision has been changed. If you are using continuous deployment with Capistrano you have probably seen this line:

bundle exec cap -S revision=$REVISION production deploy

REVISION is an environment variable that Semaphore exports during deployment and Capistrano 2 was using it as a parameter. In Capistrano 3 this is gone and you have to take care of that by setting the branch variable to revision or branch that you want to deploy. We already had in our configuration ability to specify branch through environment variable:

set :branch, ENV["BRANCH_NAME"] || "master"

so we just had to prepend ENV["REVISION"] to that chain.

set :branch, ENV["REVISION"] || ENV["BRANCH_NAME"] || "master"

This is one of the things that is not documented and you either have to dig it up in source or ask somewhere. All in all the change should be pretty straightforward.

File below is config/deploy/production.rb.

Capistrano 2:

server "server1.example.com", :app, :web, :db, :primary => true, :jobs => true
server "server2.example.com", :app, :web, :jobs => true

set :branch, ENV["BRANCH_NAME"] || "master"

Capistrano 3:

set :stage, :production

server "server1.example.com", user: "deploy_user", roles: %w{web app db}
server "server2.example.com", user: "deploy_user", roles: %w{web app}

set :branch, ENV["REVISION"] || ENV["BRANCH_NAME"] || "master"

Main configuration - config/deploy.rb

Biggest changes you will have to make will be in this file. I will list the changes that you would need to pay attention to.

  1. You no longer need to require capistrano/ext/multistage or bundler/capistrano. Multistage is included by default and bundler support is included in Capfile.
  2. No need to specify available stages or default_stage.
  3. Variable name for setting repository url has changed from repository to repo_url.
  4. deploy_via :remote_cache is not needed any more. There have been large changes under the hood in a way how Capistrano handles repositories. It now creates a local mirror of the repository on your server.
  5. PTY option is on by default.
  6. ssh_options have changed slightly I think, but basic settings are pretty much the same.
  7. Capistrano will now take care of all symlinks that you need. Just tell it to go through linked_files and linked_dirs.
  8. In case that you are not using rvm or rbenv you will need to override rake and rails commands. (See Capistrano 3 deploy.rb file)

Writing custom tasks has changed significantly and you will have to dig deeper into documentation to write tasks that you need. The library responsible for this is SSHKit. It seems like a quite nice library.

Pro-tip: In Capistrano 2 you could just write var_name and get the value. In new version you always need to write fetch(:var_name). It took me some time to figure this out while I was re-writing a custom task that we use to manage our workers.

Capistrano 2:

require "capistrano/ext/multistage" #1
require "bundler/capistrano"

set :application, "webapp"
set :stages, %w(production staging)
set :default_stage, "staging" #2

set :scm, :git
set :repository,  "git@github.com:example/webapp.git" #3
set :deploy_to, "/home/deploy_user/webapp"
set :deploy_via, :remote_cache #4

default_run_options[:pty] = true #5
set :user, "deploy_user"
set :use_sudo, false

ssh_options[:forward_agent] = true #6
ssh_options[:port] = 3456

set :keep_releases, 20

namespace :deploy do

  desc "Restart application"
  task :restart, :roles => :app, :except => { :no_release => true } do
    run "#{try_sudo} touch #{File.join(current_path,'tmp','restart.txt')}"
  end

  desc "Prepare our symlinks" #7
  task :post_symlink, :roles => :app, :except => { :no_release => true } do
    ["config/database.yml", "config/config.yml"].each do |path|
      run "ln -fs #{shared_path}/#{path} #{release_path}/#{path}"
    end
  end

end

after  "deploy",                   "deploy:post_symlink"
after  "deploy:restart",           "deploy:cleanup"
before "deploy:assets:precompile", "deploy:post_symlink"

Capistrano 3:

set :application, "webapp"

set :scm, :git
set :repo_url,  "git@github.com:example/webapp.git"
set :deploy_to, "/home/deploy_user/webapp"

set :ssh_options, {
  forward_agent: true,
  port: 3456
}

set :log_level, :info

set :linked_files, %w{config/database.yml config/config.yml}
set :linked_dirs, %w{bin log tmp vendor/bundle public/system}

SSHKit.config.command_map[:rake]  = "bundle exec rake" #8
SSHKit.config.command_map[:rails] = "bundle exec rails"

set :keep_releases, 20

namespace :deploy do

  desc "Restart application"
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      execute :touch, release_path.join("tmp/restart.txt")
    end
  end

  after :finishing, "deploy:cleanup"

end

Conclusion

The code that you get in the end is cleaner and Capistrano 3 together with SSHKit seems like a powerful combo. However some libraries like whenever and bugsnag don’t have Capistrano 3 support yet, so for now you will have to take care of that part on your own.