WebDevChallenges Logo
WebDevChallenges

How to Deploy a Rails 6 Application with Capistrano, Nginx, Puma, Postgresql, LetsEncrypt on Ubuntu 20.04

Updated June 5, 21
How to Deploy a Rails 6.1 Application with Capistrano. Using Nginx, Puma, Ubuntu 20.04, Postgresql, LetsEncrypt and Capistrano.

What are we going to do

So whenever I start a new project and want to deploy it to production, I need to research from scratch how to setup a Ubuntu Server including Firewall, how to setup Capistrano, get NGINX to work with Puma etc. That’s why I summarize everything I do to get a Project deployed.

Create the Rails project

Make sure you have postgresql installed and running locally.

rails -v
# Rails 6.1.3.2
rails new mysite --database=postgresql
rake db:setup
rails db:migrate
rails s

Now you should be able to visit http://localhost:3000 in your browser.

Server Setup

First of all, purchase a Ubuntu 20.04 Server from your favorite host. I always use Hetzner for my Projects. Their servers start at 2,96€ per Month for 1vCPU, 2GB of RAM and a 20GB local SSD. You can use my referral link if you want to check it out too.

SSH Config

I always make sure to select my public key when I create the server so that one is already entered in the ~/.ssh/authorized_keys file for the root user.

If you provide an SSH key, Hetzner will automatically disable Password authentication which provides an extra layer of security.

Make sure your /etc/ssh/sshd_config file does not allow password authentication (#PasswordAuthentication yes see the hash at the start of the line).

If you would need to change that, make sure to restart the ssh daemon afterwards (systemctl restart sshd).

Set the A record

Once you purchased your server, you will receive a IPv4 Address. When you already purchased a domain for your project, make sure to point the A Record of that domain to your newly purchased Server’s IPv4 Address. I always do that before setting up my server because that might take a few minutes to propagate through the DNS Servers.

This A record needs to be fully propagated once we try to aquire LetsEncrypt certificates later on.

Update packages

Run

apt-get update

to update your packages.

Firewall setup

Install ufw (uncomplicated firewall) and allow ssh, http and https.

apt-get install ufw
ufw status
ufw allow ssh
ufw allow http
ufw allow https
ufw enable
ufw status

Create the rails user

Let’s create a new user called rails just for running the application.

adduser rails

This will create an interactive input for creating the user, set a password and just confirm the rest (ENTER).

We want to be able to ssh into our rails user. Lets copy our ~/.ssh/authorized_keys file from the root user.

mkdir -p /home/rails/.ssh
cp ~/.ssh/authorized_keys /home/rails/.ssh
chown -R rails:rails /home/rails/.ssh/

This user needs to be a sudo user in order to restart the puma systemctl service we are going to create later on.

usermod -aG sudo rails
vi /etc/sudoers
# add the following line at the bottom
rails  ALL=(ALL) NOPASSWD: ALL

You should be able to ssh into your server with the rails user now.

ssh rails@mysite.com

Install postgres

apt-get install postgresql postgresql-contrib libpq-dev
su postgres
cd
createdb mysite_production
psql
create user rails with password 'mypassword';
grant all privileges on database mysite_production to rails;
exit # exit psql shell
exit # back to root user

Install more dependencies

Next we need to install more dependencies

  • Node
  • Yarn
  • RVM
  • NGINX

Node & Yarn

# see https://github.com/nodesource/distributions/blob/master/README.md
curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash -
apt-get install -y nodejs
node -v
npm install --global yarn
yarn -v

RVM

su rails
cd
# see https://rvm.io/rvm/install
gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable --ruby

Check on your local machine which ruby version is used in your project (3.0.1 for me)

cd mysite
cat Gemfile | grep ruby

Then install that same version on the server using RVM.

# as root
/home/rails/.rvm/bin/rvm install ruby-3.0.1
su rails
cd
source ~/.rvm/scripts/rvm
rvm use ruby-3.0.1
rvm use --default ruby-3.0.1
rvm -v
ruby -v

Install nginx

apt-get install nginx
systemctl start nginx
systemctl enable nginx

Install nginx

apt-get install git

Install capistrano

Add the following gems to your group :development do block inside the Gemfile.

gem 'capistrano',         require: false
gem 'capistrano-rvm',     require: false
gem 'capistrano-rails',   require: false
gem 'capistrano-bundler', require: false
gem 'capistrano3-puma',   require: false

Install the gems and install cap.

bundle install
cap install

Add the following to your Capfile

...
require "capistrano/rails"
require "capistrano/bundler"
require "capistrano/rvm"
require 'capistrano/puma'
install_plugin Capistrano::Puma
install_plugin Capistrano::Puma::Systemd
...

Adjust your config/deploy.rb file to look like this

lock "~> 3.16.0"

# replace obvious parts
server 'mysite.com', port: 22, roles: [:web, :app, :db], primary: true
set :application, "mysite"
set :repo_url, "git@github.com:me/mysite.git"

set :user, 'rails'
set :puma_threads,    [4, 16]
set :puma_workers,    0

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.access.log"
set :puma_error_log,  "#{release_path}/log/puma.error.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

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

Commit and push your changes.

Copy your master.key to the shared dir.

ssh rails@mysite.com
mkdir -p apps/mysite/shared/config
# back on your machine
cd mysite
scp config/master.key rails@mysite.com:apps/mysite/shared/config

Adjust your config/database.yml file.

production:
  <<: *default
  database: mysite_production
  host: localhost
  username: rails
  password: mypassword

Test a production deploy

cap production deploy

If you are on mac os, you might encounter this error.

Your bundle only supports platforms ["x86_64-darwin-19"] but your local platform is x86_64-linux. Add the current platform to the lockfile with 'bundle lock --add-platform x86_64-linux' and try again.

In that case just run

bundle lock --add-platform x86_64-linux
# commit & push

Puma systemd service

The deploy should fail in the end with the message Failed to restart puma_mysite_production.service: Unit puma_mysite_production.service not found.

So let’s create this service.

vi /etc/systemd/system/puma_mysite_production.service

Enter the following

[Unit]
Description=Puma HTTP Server for mysite (production)
After=network.target

[Service]
Type=simple
User=rails
WorkingDirectory=/home/rails/apps/mysite/current
ExecStart=/home/rails/.rvm/bin/rvm default do bundle exec puma -C /home/rails/apps/mysite/shared/puma.rb
ExecReload=/bin/kill -TSTP $MAINPID
StandardOutput=append:/home/rails/apps/mysite/current/log/puma.access.log
StandardError=append:/home/rails/apps/mysite/current/log/puma.error.log
Restart=always
RestartSec=1
SyslogIdentifier=puma

[Install]
WantedBy=multi-user.target

Create the directory for the puma sockets to live in:

mkdir apps/mysite/shared/tmp/sockets

You should be able to run the service now.

systemctl start puma_mysite_production.service
systemctl enable puma_mysite_production.service
systemctl status puma_mysite_production.service

Try to deploy again, this time it should work fine.

cap production deploy

NGINX Setup

We need a webserver to proxy http and https requests to puma. NGINX does this nicely.

vi /etc/nginx/sites-enabled/mysite

Add the following

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

server {
  server_name mysite.com;

  root /home/rails/apps/mysite/current/public;
  access_log /home/rails/apps/mysite/current/log/nginx.access.log;
  error_log /home/rails/apps/mysite/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_set_header  X-Forwarded-Proto $scheme;
    proxy_set_header  X-Forwarded-Ssl on; # Optional
    proxy_set_header  X-Forwarded-Port $server_port;
    proxy_set_header  X-Forwarded-Host $host;

    proxy_redirect off;

    proxy_pass http://puma;
  }

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

Check if the config is valid & restart nginx.

nginx -t
systemctl restart nginx

In case you get a permissions error, running the following should fix that:

sudo chown rails:rails -R apps/mysite/

Adding LetsEncrypt

You should be able to access your page now via http.

You need SSL Certificates in order to run your site via https. LetsEncrypt is free and easy to setup with nginx.

apt-get install certbot python3-certbot-nginx
certbot --nginx

Follow the interactive installer, I always choose redirect in the end and you should probably too. Refresh your page, you should be redirect to https now.

Debugging

Errors can occurr in a few places here are a few hints:

# check if nginx is running
systemctl status nginx
journalctl -u nginx

# check if puma is running
systemctl status puma_mysite_production.service
journalctl -u puma_mysite_production.service

# application logs
tail -f apps/mysite/current/log/*

Hope you find it useful. Have a nice day ;)