Deploying Rails apps using Capistrano

Capistrano Logo Deploying your application to the web is not a one-time thing. There will be changes to the application code, new assets to include, database updates, and so on. Applying these changes to your web server includes a list of commands that we need to do every time we need to deploy.

Capistrano solves this problem by automating the deploy process so you can execute all of the steps in a single command. Automation prevents user errors while deploying like forgetting the precompile task when assets have changed, missing database migrations, and remembering the sequence of commands. Automation also enables your infrastructure to scale so that your code is automatically deployed to all application servers without logging in to each one to manually deploy your code.

In order to use Capistrano, you need to set up your web server first (we will use Nginx in this article). If you have already done this, please skip the first section of the article and proceed to installing and setting up Capistrano.

Note: This article assumes that you have already set up your server and are using Ruby on Rails as your application framework.

Nginx

Installation

We will install Nginx via the Passenger gem.

In order to do this, we need root privileges. If you are using RVM, it already provides a sudo command called rvmsudo, but if you are using rbenv, you need to install the rbenv-sudo plugin first:

git clone git://github.com/dcarley/rbenv-sudo.git ~/.rbenv/plugins/rbenv-sudo

We then install the passenger gem by adding this line to our Gemfile:

gem 'passenger'

Then install the gem and its binaries:

bundle install

And if you are using rbenv, update with the newly installed binaries:

rbenv rehash

We are now ready to install Nginx via Passenger:

If you are using RVM:

rvmsudo passenger-install-nginx-module

If you are using rbenv:

rbenv sudo passenger-install-nginx-module

Compared to installing Nginx via the Ubuntu package manager (sudo apt-get install nginx) this method of installing Nginx does not provide the start and stop scripts so we can conveniently control the web server process. Thus we need to manually provide this init script in our system:

wget -O init-deb.sh https://raw.github.com/JasonGiedymin/nginx-init-ubuntu/master/nginx
sudo mv init-deb.sh /etc/init.d/nginx

There is a small change that needs to be done in our init script, which is to specify the location of the Nginx install. By default this is located at /opt/nginx, so we set it accordingly:

sudo nano /etc/init.d/nginx

NGINXPATH=${NGINXPATH:-/opt/nginx}      # root path where installed

We make our init script executable:

sudo chmod +x /etc/init.d/nginx

Then we also set it so that Nginx runs automatically when you reboot the system:

sudo /usr/sbin/update-rc.d -f nginx defaults

Rails application server block

We will place all our application code in /var/www/. Make sure that the deploy user that we created has read and write access to this directory.

Next we need to configure Nginx so that it runs our code in /var/www/. Nginx provides a simple way to organize our server configuration in server “blocks” which is very useful especially if we are running multiple applications in the same server. It will look for additonal configuration in the /etc/nginx/sites-enabled directory and includes it when running the web processes.

However, we will not be putting our configuration directly in /etc/nginx/sites-enabled, instead we will create them in the /etc/nginx/sites-available directory. In this example, we assume we have a Rails application called myrailsapp.

Create and edit a new file called myrailsapp.conf in the sites-available directory:

sudo nano /etc/nginx/sites-available/myrailsapp.conf

In the empty file, put in the following:

server {
  listen       80;
  server_name  mydomain.com www.mydomain.com;
  root /var/www/myrailsapp/current/public;
  # passenger_min_instances 4;

  location / {
    passenger_enabled on;
  }
}

If you are using SSL certificates for your domain, place the certificate/certificate bundle and the key file in the /etc/nginx/ssl directory, then add the following as well in the myrailsapp.conf file:

server {
    listen       443;
    server_name  mydomain.com www.mydomain.com;
    root /var/www/myrailsapp/current/public;
    # passenger_min_instances 4;

    ssl                  on;
    ssl_certificate      /etc/nginx/ssl/mydomain_bundle.crt;
    ssl_certificate_key  /etc/nginx/ssl/mydomain.com.key;
    
    # Forward secrecy settings
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS +RC4 RC4";

    location / {
       passenger_enabled on;
    }
}

Note that you need separate server blocks for HTTP requests (port 80) and HTTPS requests (port 443). We also set the HTTPS block to only support TLS, as the SSL protocol is already outdated and is insecure. The SSL ciphers are also set to exclude older, insecure cipher suites.

After creating the myrailsapp.conf file, we now create a symbolic link of that file to the /etc/nginx/sites-enabled directory. This is done so that when we want to disable a server block, we only need to delete the symbolic link in the sites-enabled directory without the need to modify or delete our original configuration in the sites-available directory.

sudo ln -s /etc/nginx/sites-available/myrailsapp.conf /etc/nginx/sites-enabled/myrailsapp.conf

Capistrano

Now that we have Nginx installed and our server blocks ready, it is time to set up Capistrano so we can do automated deploys to our servers.

In your Gemfile, add the Capistrano gem and other supporting gems depending on what other libraries we use in our application:

# Use Capistrano for deployment
gem 'capistrano'
gem 'capistrano-bundler'
gem 'capistrano-rvm'    # If you are using RVM
gem 'capistrano-rbenv'  # If you are using rbenv
gem 'capistrano-passenger'
gem 'capistrano-rails', group: :development

Install the gems:

bundle install

Generate the Capistrano configuration files:

bundle exec cap install

This will create the following files:

  • Capfile – for specifying modules to be included in Capistrano
  • config/deploy.rb – application-wide deploy settings
  • config/deploy/production.rb – deploy settings specific to the production environment
  • config/deploy/staging.rb – deploy settings specific to the staging environment
  • lib/capistrano/tasks – for including custom rake tasks for deployment

Application-specific configuration

Let’s start by updating the Capfile to require all modules that we need for the application. Typically this will mirror what supporting gems we have included in the Gemfile. This also loads any custom rake tasks we include in lib/capistrano/tasks.

# Load DSL and set up stages
require 'capistrano/setup'

# Include default deployment tasks
require 'capistrano/deploy'

# Include tasks from other gems included in your Gemfile
# require 'capistrano/rvm'     # Uncomment if you are using RVM
# require 'capistrano/rbenv'   # Uncomment if you are using rbenv
require 'capistrano/bundler'
require 'capistrano/rails'
require 'capistrano/passenger'

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

Next we update config/deploy.rb to set our application-wide deploy settings:

# config valid only for current version of Capistrano
lock '3.4.0'

set :application, 'myrailsapp'
set :repo_url, '[email protected]:myrailsapp.git'

set :stages, ["staging", "production"]

# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/application.yml')

# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

# Configuration for rvm
set :rvm_type, :user
set :rvm_ruby_version, '2.2.2'

# Configuration for passenger
set :passenger_restart_with_sudo, true
set :passenger_restart_command, 'service nginx restart'
set :passenger_restart_options, ''

namespace :deploy do
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end
end

Let’s take a look at each individual configuration:

set :application, 'myrailsapp'
set :repo_url, '[email protected]:myrailsapp.git'

This sets the application name and where the code repository is fetched. By default, Capistrano assumes that you are using git as your SCM, but you are free to use any other types. If you are hosting your own git server, just replace github.com with your domain or IP address in the repo_url.

# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/application.yml')

Linked files are used to persist certain files in the application so that deploying does not erase or override their values. Common files that needs to be set as a linked file is the database configuration (config/database.yml) or any environment-setting libraries you use like dotenv or Figaro (config/application.yml). These files are stored in a “shared” directory, then gets copied over to the specific deploy directory during the deploy process.

# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/uploads')

Similar to linked files, linked directories are used to persist certain directories in the application between deploys. Common directories include the log and tmp directories, as well as the location of file uploads if you are storing them in the server itself (in this example in public/uploads).

# Configuration for rvm
set :rvm_type, :user
set :rvm_ruby_version, '2.2.2'

Here we specify the Ruby version we are using for the application, assuming we are using RVM. It is important to specify the correct Ruby version here so that the deploy process will work.

# Configuration for passenger
set :passenger_restart_with_sudo, true
set :passenger_restart_command, 'service nginx restart'
set :passenger_restart_options, ''

This sets the command that we use to restart Nginx. It is possible to use the init script /etc/init.d/nginx restart command, however in this example we will use the service nginx restart command. These commands work only using sudo, so we set passenger_restart_with_sudo to true.

Environment-specific configuration

We can specify different configurations depending on the environment, like setting a separate staging server IP address or a separate test branch. These settings are saved in the config/deploy/production.rb or config/deploy/staging.rb files.

An example of an environment-specific configuration is:

server '123.123.12.1', user: 'deploy', roles: %w{web app db}

set :deploy_to, '/var/www/myrailsapp'
set :domain, 'test.myrailsapp.com'
set :rails_env, 'staging'
set :branch, 'test'

In this example, we indicate that the deploy user will be used to access the server located at IP address 123.123.12.1. We also set that this user can manage the web server (web), application (app), and migration (db) components. In cases where the database is located on a different server, the db component will be on a separate line:

server '123.123.12.1', user: 'deploy', roles: %w{web app}
server '124.124.12.1', user: 'deploy', roles: %w{db}

The deploy_to parameter sets the location where our code will be in the server, in this example, the code will be on /var/www/myrailsapp.

The rails_env parameter sets the Rails environment (production or staging), and the branch parameter allows you to specify where the code will be pulled from depending on the environment. For example, the production environment will pull from the master branch, while the staging environment will pull from the test branch. By default Capistrano will use the master branch when deploying.

Deploy commands

Now that we have configured Capistrano, we can now test if everything works by running:

bundle exec cap production deploy --dry-run

This will tell us if there is something wrong with our set up, and the –dry-run flag will not do the actual deploy, allowing us to simulate the whole deploy process.

One common error is when the linked files are missing. In this case we need to ensure that this directory exists:

/var/www/myrailsapp/shared/config

And make sure we create the database.yml and application.yml files in there.

Once that is in place we are now ready to do the actual deploy:

bundle exec cap production deploy

SSH credentials

At the beginning of the deploy process, the Capistrano script will attempt to establish an SSH connection to the server in order to execute commands. This means that you will need to input your SSH password every time you deploy.

To fully achieve automated deploys, you will need to add your SSH public key to the authorized keys list in your server so that you no longer need to input your SSH password every time you connect to the server.

Your SSH public key is located at:

~/.ssh/id_rsa.pub

If this file does not exist yet, you can generate it using this command:

ssh-keygen -t rsa

It will prompt you to enter your passphrase, or you can just press Enter for all of the optional configurations and it will still work (albeit less secure).

Once you have your SSH public key in place, add this to your server’s authorized_keys list:

cat ~/.ssh/id_rsa.pub | ssh -p 22 [email protected] 'cat >> ~/.ssh/authorized_keys'

Replace the SSH port (22) if you are not using the default SSH port, and replace 123.123.12.1 with your server’s IP address or domain.

Passwordless commands

Towards the end of the Capistrano deploy script, it will try to restart Nginx to reload your applications. Restarting Nginx requires sudo privileges, and you will be asked to provide your sudo password before it can restart the web server.

If you do not want the deploy process to be interrupted by asking the sudo password, you can specify which commands no longer need a sudo password so your deploy user can directly call them. To do this, log in to your server and log in as root:

ssh [email protected]
sudo su

Once you are logged in as root, modify the sudoers file and add the permission for restarting Nginx and other services:

visudo

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL

deploy ALL = NOPASSWD: /usr/sbin/service

Save the file, and you can test if the changes worked by logging out of the server, logging in again, and restarting Nginx:

sudo service nginx restart

If you are not prompted for your sudo password, then your changes to the sudoers file has worked.

If you added your SSH public key to your server and updated the sudoers file for passwordless sudo commands, your Capistrano set up is now fully automated. To deploy your application, just use this command, sit back and let Capistrano do all the work!

bundle exec cap production deploy

Custom Tasks

Capistrano also enables you to specify custom tasks. You can hook these tasks into the deploy process so that it automatically gets included when you deploy, or you can run them separately. In this example we will add a custom task that runs the Rails seed command:

bundle exec rake db:seed

You can put the custom task in config/deploy.rb or in lib/capistrano/tasks. In this example we will update deploy.rb and put in the seed task:

namespace :deploy do
  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end
    
  task :seed do
    on primary fetch(:migration_role) do
      within release_path do
        with rails_env: fetch(:rails_env)  do
          execute :rake, 'db:seed'
        end
      end
    end
  end
end

After saving the file, we can now run the Rails seed task in our server via Capistrano:

bundle exec cap production deploy:seed

Leave a Reply

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