Deploying Rails app using Nginx, Puma and Capistrano 3

Start by creating a Droplet (Ubuntu 14.04 LTS x64) and ssh to root in terminal:

ssh [email protected]              # replace 'server' with your vps ip

Change your password:

passwd

Create a new user and set privileges

adduser username
visudo

Find user privileges section, and duplicate the root line changing it to your new user:

# User privilege specification
root     ALL=(ALL:ALL) ALL
username ALL=(ALL:ALL) ALL

Press (CTRL+O), then (CTRL+X) to save and quit. Now it's time to configure ssh:

nano /etc/ssh/sshd_config

For better security, it's recommended that you disable root and change the ssh port (anything between 1025..65536):

Port 22 # change this to whatever port you wish to use
Protocol 2
PermitRootLogin no

At the end of sshd_config, enter:

UseDNS no
AllowUsers username

Press (CTRL+O), then (CTRL+X) to save and quit. Reload ssh:

reload ssh

Don't close root yet! Open a new shell and ssh to vps with new username (remember the port, or you're locked out!)

ssh [email protected] -p 7171

If everything's working fine, you can close root. We need to install packages now; in your new user ssh session, enter:

sudo apt-get update
sudo apt-get install curl git-core nginx -y

Install rvm, ruby and rubygems:

curl -sSL https://get.rvm.io | bash -s stable
source ~/.rvm/scripts/rvm
rvm requirements
rvm install 2.1.0
rvm use 2.1.0 --default
rvm rubygems current

(RVM installation might fail, and it may ask you to download its GPG Keys, so do that and retry)

Install rails and bundler:

gem install rails --no-ri --no-rdoc -V
gem install bundler --no-ri --no-rdoc -V

Shake hands with Github/Bitbucket and Generate a public/private key pair:

ssh -T [email protected].com
ssh -T [email protected].org
ssh-keygen -t rsa 

Add it as a deployment key for your repository (Instructions: Github & Bitbucket). Also try cloning on the server manually to make sure it's working fine.

Add your own ssh key to the VPS' authorized_keys. In your local terminal session, enter:

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

Now, in your project's Gemfile, add these and bundle:

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

Run:

cap install

A Capfile in your root and a deploy.rb file in config folder will be created for you. Replace the contents of your Capfile with:

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

require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/rvm'
require 'capistrano/puma'

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

Replace the contents of config/deploy.rb with this (modify parameters according to your app):

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

set :repo_url,        '[email protected]:user/app.git'
set :application,     'appname'
set :user,            'username'
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, false  # Change to true if 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{bin 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

  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      invoke 'puma:restart'
    end
  end

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

# ps aux | grep puma    # Get puma pid
# kill -s SIGUSR2 pid   # Restart puma
# kill -s SIGTERM pid   # Stop puma

Create config/nginx.conf in your project directory and add this to it (again replacing with your parameters):

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

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

  root /home/username/apps/appname/current/public;
  access_log /home/username/apps/appname/current/log/nginx.access.log;
  error_log /home/username/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;
}

Commit changes, push and make the initial deploy:

git add -A
git commit -m "Set up Puma, Nginx & Capistrano"
git push origin master
cap production deploy:initial

If everything goes smoothly, back in your ssh session, symlink your nginx.conf and start nginx. You only need to do this once (but you might have to restart nginx whenever you update your conf file).

sudo rm /etc/nginx/sites-enabled/default
sudo ln -nfs /home/username/apps/appname/current/config/nginx.conf /etc/nginx/sites-enabled/appname
sudo service nginx start

Now onwards, whenever you want to push a new deploy:

git add -A
git commit -m "Deploy Message"
git push
cap production deploy