Back to Posts

Deploying Rails

Preface

We will be setting up a Ruby on Rails 5.1 production environment on Ubuntu 16.04 LTS Xenial Xerus. We're going to be setting up a Droplet on Digital Ocean for our server, but you should be able to use any VPS host such as Linode. Our production environment will consist of Ruby 2.4.1, Rails 5.1, Node 6.11, Postgres 9.6.3 and Phusion Passenger 5.1.5 with Nginx. We will also install Yarn for asset management if it is being used for your project.

Initial Server Setup

Groups and Privileges

I prefer creating a new group for deploying with sudo permissions instead of just adding a new user to the sudo group.

Add the group deployers to your system

1
groupadd deployers

Create a new user for the deployers group. In this guide, I am calling the new user deploy`

1
adduser deploy -ingroup deployers

Add root privileges to the deployers group. Open /etc/sudoers in your editor.

1
nano /etc/sudoers

In /etc/sudoers add the following line at the end of the file and save your changes.

1
%deployers ALL=(ALL) ALL

Configuring SSH Access

Open /etc/ssh/sshd_config in your editor.

1
nano /etc/ssh/sshd_config

It is recommended to change the default ssh port. You will want to choose a port number between 1025 and 65536

Make the following changes to /etc/ssh/sshd_config

1
2
3
4
5
6
7
8
9
10
11
# /etc/ssh/sshd_config

...
Port 1025
...
# Change PermitRootLogin to no
# After changing this, you will need to log in as your 'deploy' user and switch to root
PermitRootLogin no

# It is also a good idea to only allow specific users to login. Add the following at the end of the file
AllowUsers deploy

Save changes to /etc/ssh/sshd_config and restart the SSH service

1
sudo service ssh restart

Log in with your new user in a new terminal window/tab. Don't worry if you are not able to login just yet.

1
ssh -p 1025 deploy@111.222.333.444

Enabling SSH Authentication

Do This on your local computer and not on your VPS

You will want to check if your local machine has an ssh-key

1
ls ~/.ssh

If you see any of the following .pub files after running the previous command, then skip the following block of code.

1
2
3
4
id_dsa.pub
id_ecdsa.pub
id_ed25519.pub
id_rsa.pub

If you did not have a public ssh key, do the following

1
2
3
4
5
6
7
8
9
10
11
# Create a new ssh key, using your  email address as a label and 4096 bits long
ssh-keygen -t rsa -b 4096 -C "your.email@example.com"

# Start the ssh-agent in the background
eval "$(ssh-agent -s)"

# It should return the pid of the agent
# Agent pid 59566

# Add your ssh key to the ssh-agent
ssh-add ~/.ssh/id_rsa

Once you have created your ssh-key or if you already had one, you will need to add your public ssh-key to our deploy user.

The easiest way add your public ssh-key to your deploy user is with ssh-copy-id. If ssh-copy-id is not working I have also provided instructions on how to do it manually.

shh-copy-id

If you do not have ssh-copy-id you can install it through Homebrew if you are on a Mac.

1
ssh-copy-id -i ~/.ssh/id_rsa.pub -p 1025 deploy@111.222.333.444

Manually

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# On your local machine you will want to copy your public ssh-key. Use pbcopy to copy your public ssh-key to your clipboard

pbcopy < ~/.ssh/id_rsa.pub

# Log into your deploy user 
# *If you could log into your deploy user you will need to log into root first and then switch to your deploy user
# You can switch from root to your deploy user using: su - deploy_user

# On your deploy user do the following:
mkdir ~/.ssh
touch ~/.ssh/authorized_keys

# Open ~/.ssh/authorized_keys with your editor and paste in your public ssh-key
nano ~/.ssh/authorized_keys

Configure Timezones and Network Time Protocol Synchronization

1
2
# This will change /etc/localtime to the timezone of your choose.
sudo dpkg-reconfigure tzdata

Configure NTP Synchronization

1
2
3
# This will allow your computer to stay in sync with other servers, leading to more predictability in operations that rely on having the correct time.
sudo apt-get update
sudo apt-get install ntp

Create a Swap File

Advice about the best size for a swap space varies significantly depending on the source consulted. Generally, an amount equal to or double the amount of RAM on your system is a good starting point.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Pre-allocate space for /swapfile in bytes
sudo fallocate -l 1G /swapfile

# Restrict access to only the owner of the file
sudo chmod 600 /swapfile

# Tell the system to format swapfile
sudo mkswap /swapfile

# Tell system it can use swapfile
sudo swapon /swapfile

# Have the system make use of swapfile on boot
sudo sh -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'

At this point, it would be a good idea to create a snapshot/backup of your configuration.


Prerequisites Before Deployment

Installing Mail (optional)

This step is optional and will allow you to directly send emails from your server. Aside from this option you can use a service like Sendgrid or Gmail

1
sudo apt-get -y install telnet postfix

Dependencies

1
sudo apt-get -y install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev python-software-properties libffi-dev

Installing Node.js

1
2
3
# Installs the current LTS version of Node and NPM
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install -y nodejs

Installing Yarn

1
2
3
4
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

Installing rbenv

1
2
3
4
5
6
7
8
git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL

git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
exec $SHELL

Installing Ruby

1
2
3
rbenv install 2.4.1
rbenv global 2.4.1
ruby -v

Installing Bundler

1
2
3
4
5
6
# Don't install rdoc or ri to speed up gem installs
echo "gem: --no-ri --no-rdoc" > ~/.gemrc

gem install bundler

rbenv rehash

Installing PostgreSQL

1
2
3
4
# We will add a new repository to grab the latest version of PostgreSQL
# As of this writing this installs PostgreSQL 9.6

sudo su -c "echo 'deb http://apt.postgresql.org/pub/repos/apt/ xenial-pgdg main' > /etc/apt/sources.list.d/pgdg.list"

Import the repository signing key, and update the package lists

1
2
3
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
  sudo apt-key add -
sudo apt-get update

Install PostgreSQL and its dependencies

1
sudo apt-get install postgresql postgresql-contrib libpq-dev

Log into user postgres

1
sudo -u postgres psql

Once logged in, setup a password for postgres user:

1
\password

We’ll also create a new user called admin with the password secret, followed by a database called yourappname, which will be owned by admin

1
2
create user admin with password 'secret';
create database yourappname owner admin;

Exit psql with

1
\q

Nginx and Phusion Passenger

1
2
3
4
5
6
7
8
9
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates

# Add the passenger APT repository
sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger xenial main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update

# Install Passenger + Nginx
sudo apt-get install -y nginx-extras passenger

Edit /etc/nginx/nginx.conf and uncomment include /etc/nginx/passenger.conf; and save your changes when you are done.

Then edit /etc/nginx/passenger.conf to modify the ruby line to:

1
passenger_ruby /home/deploy/.rbenv/shims/ruby;

Save the changes you made to /etc/nginx/passenger.conf and restart nginx

1
sudo service nginx restart

You may want to create another snapshot/backup at this point


Setting Up Capistrano

Setting up SSH Keys for Repo

There are many ways to setup how your deployment will interact with your server. In this guide we are going to use Agent Forwarding You can read more about the different methods here:

If You have not done so yet on your VPS, go ahead and 'shake hands' with Github. Don't worry if you get a permission denied error.

1
ssh -T git@github.com

Deployment Configurations for the Rails App

In your Gemfile add the following:

1
2
3
4
5
6
7
8
9
10
11
# Gemfile

...
group :development do
  ...
  gem 'capistrano', require: false
  gem 'capistrano-rbenv', require: false
  gem 'capistrano-rails', require: false
  gem 'capistrano-bundler', require: false
  gem 'capistrano-passenger', require: false
end

And then run the following commands:

1
2
bundle install
cap install

cap install will create the following files:

1
2
3
4
5
6
7
Capfile # in the root directory of your Rails app

deploy.rb # in the config directory

deploy directory # in the config directory

# In the config/deploy folder, it will also create a staging.rb and production.rb file

Open the Capfile and change it to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Load DSL and set up stages
require "capistrano/setup"

# Include default deployment tasks
require "capistrano/deploy"

# Load the SCM plugin appropriate to your project:
#
# require "capistrano/scm/hg"
# install_plugin Capistrano::SCM::Hg
# or
# require "capistrano/scm/svn"
# install_plugin Capistrano::SCM::Svn
# or
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

# Include tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
#   https://github.com/capistrano/rvm
#   https://github.com/capistrano/rbenv
#   https://github.com/capistrano/chruby
#   https://github.com/capistrano/bundler
#   https://github.com/capistrano/rails
#   https://github.com/capistrano/passenger
#
# require "capistrano/rvm"
require "capistrano/rbenv"
# require "capistrano/chruby"
require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
require "capistrano/passenger"
require "airbrussh/capistrano"

# Load 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 the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# config valid only for current version of Capistrano
lock "3.8.2"

set :application, "your_app_name"
set :repo_url, "git@github.com:username/repo_name.git"

set :deploy_user, "deploy_username"

# Default branch is :master
# ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp

# Default deploy_to directory is /var/www/my_app_name
set :deploy_to, "/home/#{fetch(:deploy_user)}/#{fetch(:application)}"

# Default value for :format is :airbrussh.
# set :format, :airbrussh

# You can configure the Airbrussh format using :format_options.
# These are the defaults.
# set :format_options, command_output: true, log_file: "log/capistrano.log", color: :auto, truncate: :auto

# Default value for :pty is false
# set :pty, true

# Default value for :linked_files is []
append :linked_files, "config/database.yml", "config/secrets.yml", "config/application.yml"

# Default value for linked_dirs is []
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"

# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }

# Default value for keep_releases is 5
# set :keep_releases, 5

# Rbenv
set :rbenv_type, :user
set :rbenv_ruby, "2.4.1"
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all

# Rails
set :keep_assets, 2

# Nginx
set :nginx_config_name, -> { "#{fetch(:application)}_#{fetch(:stage)}" }

Replace the contents of config/deploy/production.rb with the following, updating fields your app and Droplet parameters:

1
2
3
set :ssh_options, user: your_deploy_user, port: 12345

server 111.222.333.444, roles: [:web, :app, :db], primary: true

Create the following two files deploy.rake and nginx.rake in lib/capistrano/tasks directory

Open up deploy.rake and add the following tasks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# deploy.rake

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 'Upload config files.'
  task :upload_yml do
    on roles(:app) do
      execute "mkdir #{shared_path}/config -p"
      upload! StringIO.new(File.read('config/database.yml')), "#{shared_path}/config/database.yml", via: :scp
      upload! StringIO.new(File.read('config/application.yml')), "#{shared_path}/config/application.yml", via: :scp
      upload! StringIO.new(File.read("config/secrets.yml")), "#{shared_path}/config/secrets.yml", via: :scp
    end
  end

  before :deploy, 'deploy:check_revision'
end

Open nginx.rake and add the following task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# nginx.rake

namespace :nginx do
  desc 'Setup nginx configuration'
  task :upload_config do
    on roles :web do
      upload! StringIO.new(File.read('config/nginx.conf')),
        "/tmp/#{fetch(:nginx_config_name)}"
      execute :mv,
        "/tmp/#{fetch(:nginx_config_name)} /etc/nginx/sites-available/#{fetch(:nginx_config_name)}"
      execute :ln, '-fs',
        "/etc/nginx/sites-available/#{fetch(:nginx_config_name)} /etc/nginx/sites-enabled/#{fetch(:nginx_config_name)}"
    end
  end
end

Deployment Configurations for Nginx

Create config/nginx.conf in your Rails project directory, and add the following to it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
  listen 80;
  listen [::]:80 ipv6only=on;

  server_name yourdomain.com;
  passenger_enabled on;
  rails_env production;

  root /home/deploy/your_app_name/current/public;

  error_page 500 502 503 504 /50x.html;
  location = /50x.html {
    root html;
  }
}

server {
  server_name www.yourdomain.com;
  return 301 $scheme://yourdomain.com$request_uri;
}

Deploying

Before we deploy, commit your changes and push to your changes.

1
2
3
git add -A
git commit -m "Set up Capistrano"
git push origin master

First Time

You will need to change the group on who can read/write/execute for the /etc/nginx/sites-available and /etc/nginx/sites-enabled. To do this run the following commands:

1
2
3
4
5
sudo chgrp deployers /etc/nginx/sites-available
sudo chmod g+w /etc/nginx/sites-available

sudo chgrp deployers /etc/nginx/sites-enabled
sudo chmod g+w /etc/nginx/sites-enabled

Also if you did not 'shake hands' with Github on your sever, do that now as well.

1
ssh -T git@github.com

Once you have done the above, you will want to upload your nginx.conf file and your database.yml, secrets.yml, and application.yml files.

I am fan of the gem Figaro to setup my environment variables, which is why I am uploading an application.yml file.

To get your your files onto your server run the following:

1
2
3
4
5
6
7
# Upload your nginx.conf file to your server
# It will be uploaded into your sites-available folder and then symlinked to the sites-enabled folder
# File name will be: yourapplication_yourenvironmentstage e.g. mynewapp_production
cap production nginx:upload_config

# Upload your database.yml, secrets.yml and application.yml files
cap production deploy:upload_yml

After you have uploaded your files, you can finally deploy your app with:

1
cap production deploy

After a successful deploy head into your server and delete the default file in the sites-enabled folder:

1
2
3
# This needs to be deleted, so we can get rid of the default nginx html page

sudo rm /etc/nginx/sites-enabled/default

After you have deleted /etc/nginx/sites-enabled/default, restart the Nginx service:

1
sudo service nginx restart

Deploying after the initial deploy

To push any new changes to your server, you can simply run

1
cap production deploy

At this point the application should up on your VPS and publicly viewable at your domain.