How to Deploy a Rails 6 Application with Capistrano, Nginx, Puma, Postgresql, LetsEncrypt on Ubuntu 20.04
Marc
Posted on May 31, 2021
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 httpp and httpps 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 ;)
Posted on May 31, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.