Setup and deploy Rails app with Capistrano


2019.09 update: RVM installation commands

When I was a new Ruby developer, deploying a Rails app seems to be an enormous task (and I don't talk about Heroku here). Capistrano used to be a life-saver for me back then, but at that time it was still a matter of copy and paste.

Recently I have chance to use Capistrano again after a long time using Heroku. With all the tutorials I found it still seems enormous as it used to be. So I decide to write a complete guide to deploy a Rails app with Capistrano with various envinronments and settings.

The flow

Regardless of remote host OS (Ubuntu/Centos), application servers (Puma/Unicorn/Passenger) and HTTP servers (Nginx/Apache), the flow of setting up server and deploy Rails app stay the same as follow:

  1. Create deploy account
    • Create deploy account with sudo privilege
    • Generate SSH keys and add it to Github repo deployment key
    • Allow SSH from local machine
  2. Install Ruby on Rails & dependencies
  3. Capistrano
    • with Puma
    • with Unicorn [next month]
    • with Passinger/Raptor [next month]
  4. Install and config nginx

More advance stuffs: (will be added in future)

  • Custom Capistrano tasks
  • Complex Capistrano config for multiple servers deployment

1. Create deploy account on remote server

It is a good practice not to use root account as deployment account.

Usually we create a dedicated user for deployment purpose, often named deploy with sudo access.

Create deploy account with sudo privilege

As root

On Ubuntu

adduser deploy
gpasswd -a deploy sudo

On Centos

adduser deploy
# Set password for deploy user
passwd deploy
usermod -aG wheel deploy

Generate SSH keys and add it to Github repo deployment key

Now login to the new user you've created:

su - deploy

On Ubuntu

Generate new SSH key pair:

ssh-keygen

You will be ask to enter key name and passphase, just skip them.

Shake hand with Github / Bitbucket:

ssh -T [email protected]
ssh -T [email protected]

You will likely get a Permission denied (publickey) message. It is because you haven't add deploy public key to Github / Bitbucket deployment key yet.

To show deploy public key, run:

cat ~/.ssh/id_rsa.pub

Copy this key to Github / Bitbucket repo follow instruction:

Now shake hand again (e.g. Github):

ssh -T [email protected]
> Hi github-username/repo-name! You've successfully authenticated, but GitHub does not provide shell access.

If you get the message above, then everything is fine.

Allow SSH from local machine

Assume you are going to run Capistrano command from your local machine, you need to add your local machine public key to list of authorized keys on remote server.

Open this file from deploy and paste your local machine public key and save it

vi ~/.ssh/authorized_keys

To make sure this work, try to run this from your local machine:

ssh deploy@server-ip

Install Ruby on Rails & dependencies

We should use a ruby version manager to install and manage multiple Ruby versions.
There are 2 popular choices: RVM and rbenv. This article will use RVM for this purpose.

From deploy user:

On Ubuntu

# Update package index
sudo apt-get update

# Install curl
sudo apt-get install curl

# install RVM
gpg2 --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable
echo "source $HOME/.rvm/scripts/rvm" >> ~/.bash_profile
source ~/.rvm/scripts/rvm # activate RVM
rvm requirements # install dependencies

# Install Ruby 2.3.0 & Rails 4.2.6
rvm install 2.3.0
rvm use 2.3.0 --default
gem install rails -v '4.2.6' -V --no-ri --no-rdoc
gem install bundler -V --no-ri --no-rdoc

# Install git
sudo apt-get install git-core

# Install database engine based on your project
sudo apt-get install mysql-server
sudo apt-get install postgresql postgresql-contrib libpq-dev

# Install nodejs, your Rails app needs a JS runtime
sudo apt-get install nodejs

# Install yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn

# Install nginx
sudo apt-get install nginx

# Others
sudo apt-get install redis-server
sudo apt-get install imagemagick

On Centos / Amazon Linux

# Update package index
sudo yum -y update

# Install curl
sudo yum install curl

# install RVM
gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -L get.rvm.io | bash -s stable
source ~/.rvm/rvm.sh # activate RVM
rvm requirements # install dependencies

# Install Ruby 2.3.0 & Rails 4.2.6
rvm install 2.3.0
rvm use 2.3.0 --default
gem install rails -v '4.2.6' -V --no-ri --no-rdoc
gem install bundler -V --no-ri --no-rdoc

# Install git
sudo yum install git

# Install database engine based on your
sudo yum install mysql-server
sudo yum install postgresql-server postgresql-contrib postgresql-devel

# Install nodejs, your Rails app needs a JS runtime
sudo yum -y install nodejs

Capistrano

With Puma

Add following gems to Gemfile

group :development do
  gem 'capistrano',         require: false
  gem 'capistrano-rvm',     require: false
  gem 'capistrano-rails',   require: false
  gem 'capistrano-bundler', require: false
  gem 'capistrano3-puma',   require: false
end

gem 'puma'

then bundle those gems

bundle

then install Capistrano:

cap install

This will create:

  • Capfile
  • config/deploy.rb: main config about deployment process
  • config/deploy: envinronment specific config (e.g. staging.rb, production.rb)

Change the content of Capfile to:

# Load DSL and Setup Up Stages
require 'capistrano/setup'
require 'capistrano/deploy'

require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/rvm'
require 'capistrano/puma'
install_plugin Capistrano::Puma
require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git

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

And change the content of config/deploy.rb to:

# Change these
server 'your_server_ip', roles: [:web, :app, :db], primary: true

set :repo_url,        '[email protected]:username/appname.git'
set :application,     'appname'
set :user,            'deploy'
set :puma_threads,    [4, 16]
set :puma_workers,    0

# Don't change these unless you know what you're doing
set :pty,             true
set :use_sudo,        false
set :stage,           :production
set :deploy_via,      :remote_cache
set :deploy_to,       "/home/#{fetch(:user)}/apps/#{fetch(:application)}"
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.error.log"
set :puma_error_log,  "#{release_path}/log/puma.access.log"
set :ssh_options,     { forward_agent: true, user: fetch(:user), keys: %w(~/.ssh/id_rsa.pub) }
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true  # Change to false when not using ActiveRecord

## Defaults:
# set :scm,           :git
# set :branch,        :master
# set :format,        :pretty
# set :log_level,     :debug
# set :keep_releases, 5

## Linked Files & Directories (Default None):
set :linked_files, %w{config/database.yml}
set :linked_dirs,  %w{log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}

namespace :puma do
  desc 'Create Directories for Puma Pids and Socket'
  task :make_dirs do
    on roles(:app) do
      execute "mkdir #{shared_path}/tmp/sockets -p"
      execute "mkdir #{shared_path}/tmp/pids -p"
    end
  end

  before :start, :make_dirs
end

namespace :deploy do
  desc "Make sure local git is in sync with remote."
  task :check_revision do
    on roles(:app) do
      unless `git rev-parse HEAD` == `git rev-parse origin/master`
        puts "WARNING: HEAD is not the same as origin/master"
        puts "Run `git push` to sync changes."
        exit
      end
    end
  end

  desc 'Initial Deploy'
  task :initial do
    on roles(:app) do
      before 'deploy:restart', 'puma:start'
      invoke 'deploy'
    end
  end

  before :starting,     :check_revision
  after  :finishing,    :compile_assets
  after  :finishing,    :cleanup
end

For the first time, we need to run

cap production deploy:initial

From then on:

cap production deploy

Quick command to control Puma:

cap puma:restart                   # restart puma
cap puma:start                     # Start puma
cap puma:status                    # status puma
cap puma:stop                      # stop puma

For a full list of capistrano task, run:

cap -T

Config Nginx

Server block files are what specify the configuration of our separate sites and dictate how the Nginx web server will respond to various domain requests.

To begin, we will need to set up the directory that our server blocks will be stored in, as well as the directory that tells Nginx that a server block is ready to serve to visitors. The sites-available directory will keep all of our server block files, while the sites-enabled directory will hold symbolic links to server blocks that we want to publish. We can make both directories by typing:

sudo mkdir /etc/nginx/sites-available
sudo mkdir /etc/nginx/sites-enabled

nginx.conf

upstream puma {
  server unix:///home/deploy/apps/appname/shared/tmp/sockets/appname-puma.sock;
}

server {
  listen 80 default_server deferred;
  # server_name example.com;

  root /home/deploy/apps/appname/current/public;
  access_log /home/deploy/apps/appname/current/log/nginx.access.log;
  error_log /home/deploy/apps/appname/current/log/nginx.error.log info;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @puma;
  location @puma {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    proxy_pass http://puma;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 10M;
  keepalive_timeout 10;
}

Fix nginx permission denied:

The only wat I could make it works was to change this line in /etc/nginx/nginx.conf from

user nginx;

# to 

user root;

Extra tips

Add RAILS_ENV environment variable to server

Assume that we just setup our production server. Adding the following line to ~/.bashrc

export RAILS_ENV=production

or to etc/environment

RAILS_ENV=production

This will simplify Rails commands when you need to use.

# before
RAILS_ENV=production rails c

# after
rails c

Quickly create PostgreSQL user and password

If we use PostgreSQL as DB for Rails app, the first deployment often come up with following error

PG::ConnectionBad FATAL: role “deploy” does not exist

The following command allow to quickly create a PostgreSQL user and password to use in database.yml

sudo -u postgres createuser -P -d -e deploy

See more at StackOverflow