25 Nov 2020 · Semaphore News

    Capistrano 3 Upgrade Guide

    7 min read
    Contents

    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:

    console
    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:

    console
    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:

    console
    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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by: