Back

Leveling up, deploying Rails application with Capistrano

Last time we deployed a rails application using plain good ol’ scripts. Today, we will be deploying it using a more easy, and if you will, a more standard approach.

Enter, Capistrano

So what the hell is Capistrano? Well put it simply, it’s just a tool written in Ruby to handle scripts that need to be automated in a more easy and readable way. It’s a simple way of executing ssh scripts.

Requirements

First of all, you will need a server, and the following software installed on it:

If you are using Debian/Ubuntu, you are lucky. Here is a script to install everything:

# Debian/Ubuntu script
# Install nginx
# Install ruby
# Install node & yarn

# Update everything
sudo apt update && sudo apt upgrade

# Install nginx
sudo apt install nginx -y

# Install RVM
sudo apt-get install -y software-properties-common
sudo apt-add-repository -y ppa:rael-gc/rvm
sudo apt-get update -y
sudo apt-get install -y rvm postgresql-client libpq5 libpq-dev
source /etc/profile.d/rvm.sh

# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.36.0/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"

# Install ruby (you way need another version)
rvm install ruby-2.6.6

# Install bundler
gem install bundler

# Install node (you may need another version)
nvm install node

# Install yarn
npm i -g yarn

Capifying your project

Now that we got all our server’s dependencies installed, let’s add Capistrano to the project. Add the following to your Gemfile:

group :development do
  gem "capistrano", "~> 3.10", require: false
  gem "capistrano-rails", "~> 1.6", require: false
  gem 'capistrano-rvm', require: false
  gem 'capistrano-nvm', require: false
  gem 'capistrano-bundler', '~> 2.0', require: false
  gem 'capistrano3-puma', require: false
end

Run bundle install to download all the dependencies. Good, now let’s run bundle exec cap install. This will create a few files that Capistrano will use on each deploy.

Go ahead and open up the Capfile. Make sure the following lines are included:

require "capistrano/rvm"
require "capistrano/nvm"
require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
require "capistrano/puma"
install_plugin Capistrano::Puma
install_plugin Capistrano::Puma::Nginx

Good, save it, and you are done with it. Open up config/deploy.rb. And update the following:

lock "~> 3.14.1"

# Update with your app's info
set :application, "your_application"
set :repo_url, "https://github.com/your/repo.git"

append :linked_files, "config/master.key"
append :linked_dirs, ".bundle"
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "vendor/bundle", ".bundle", "public/system", "public/uploads"

# Make sure it matches with your server
# This should work, but if not, check where did you installed rvm
set :rvm_custom_path, "/usr/share/rvm"
set :rvm_ruby_string, :local

set :nvm_type, :user
# Update to your node version
set :nvm_node, "v14.13.0"
set :nvm_map_bins, %w{node npm yarn}

set :nginx_use_ssl, true
set :nginx_ssl_certificate, "/etc/ssl/certs/#{fetch(:nginx_config_name)}.pem"
set :nginx_ssl_certificate_key, "/etc/ssl/private/#{fetch(:nginx_config_name)}.pem"

We are almost there. Go ahead now and edit config/deploy/production.rb. Add the following:

server "YOUR-SERVER-IP", user: "root", roles: %w{app db web}
set :rails_env, 'production'

SSL Certificate

It’s 2020, we need our page to use HTTPS. Luckily it’s pretty easy to do so.

We will be using LetsEncrypt. Again, if you are using Debian/Ubuntu, you are in luck:

sudo snap install --classic certbot
certbot -d *.example.com -d example.com --manual --preferred-challenges dns certonly

Make sure to update example.com to use your domain.

After you run the scripts, you just need to create a new TXT entry in your DNS. This way you will be able to complete the challenge and the certificate will be generated.

Once you get the certificates, download the generated files to your machine. Make sure you download the files to your-project/cert/production/fullchain.pem and your-project/cert/production/privkey.pem. You will thank me later.

The heavy stuff is completed, now copy-paste time

Here are a few useful tasks I have created to make the whole deployment easier. Make sure to create them on lib/capistrano/tasks and use the .rake extension!

# Small little task for nvm to work on the server
# https://github.com/koenpunt/capistrano-nvm/issues/25#issuecomment-321570172
namespace :nvm do
  namespace :webpacker do
    task :validate => [:'nvm:map_bins'] do
      on release_roles(fetch(:nvm_roles)) do
        if !test('node', '--version')
          warn "node is not installed"
          exit 1
        end

        if !test('yarn', '--version')
          warn "yarn is not installed"
          exit 1
        end
      end
    end

    task :wrap => [:'nvm:map_bins'] do
      on roles(:web) do
        SSHKit.config.command_map.prefix['rake'].unshift(nvm_prefix)
      end
    end

    task :unwrap do
      on roles(:web) do
        SSHKit.config.command_map.prefix['rake'].delete(nvm_prefix)
      end
    end

    def nvm_prefix
      fetch(
        :nvm_prefix, -> {
          "#{fetch(:tmp_dir)}/#{fetch(:application)}/nvm-exec.sh"
        }
      )
    end

    after 'nvm:validate', 'nvm:webpacker:validate'
    before 'deploy:assets:precompile', 'nvm:webpacker:wrap'
    after 'deploy:assets:precompile', 'nvm:webpacker:unwrap'
  end
end
# Upload ssl certificates to the server
# based on the stage being deployed
namespace :deploy do
  namespace :check do
    before :linked_files, :upload_certs do
      on roles(:app), in: :sequence, wait: 10 do
        upload! "cert/#{fetch :stage}/fullchain.pem", fetch(:nginx_ssl_certificate)
        upload! "cert/#{fetch :stage}/privkey.pem", fetch(:nginx_ssl_certificate_key)
      end
    end
  end
end
# Upload config/master.key file to server
namespace :deploy do
  namespace :check do
    before :linked_files, :set_master_key do
      on roles(:app), in: :sequence, wait: 10 do
        unless test("[ -f #{shared_path}/config/master.key ]")
          upload! 'config/master.key', "#{shared_path}/config/master.key"
        end
      end
    end
  end
end
# Upload nginx conf to server
namespace :deploy do
  namespace :check do
    before :linked_files, :upload_nginx_conf do
      on roles(:app) do
        invoke 'puma:nginx_config'
      end
    end
  end
end

These tasks will:

On every deploy.

Deploy!

Now, with a little bit of luck, run bundle exec cap production deploy and it should work just fine. If not, I have to say, Capistrano does a pretty good job in pointing out where the error is and usually it’s quite easy to fix it.

If everything went well, log into your server and restart your nginx service. If you are on Debian/Ubuntu use sudo nginx -t && sudo systemctl reload nginx.

And that’s it! Now you can deploy more easily with Capistrano.