Deploying Rails
Published on 04/23/20
deployment
Initial Server Setup
Creating a deploy user
While logged in as root
create a deployment user.
# Create a new user
adduser deploy
# Add user to sudo group
usermod -aG sudo deploy
Next upload your SSH Key to both root
and deploy
. The simplest way to do this is via ssh-copy-id
as its available through most package managers or installed by default.
ssh-copy-id root@server_ip
ssh-copy-id deploy@server_ip
Confirm you can SSH into the server with your new user.
ssh deploy@server_ip
From this point forward, we will be using the deploy
user to configure the server.
Disabling SSH access via Root
Edit sshd_config
sudo nano /etc/ssh/sshd_config
Set PermitRootLogin
to no
PermitRootLogin no
Save your configuration and reload SSH
sudo service ssh restart
Locking down ports with UFW
sudo ufw allow OpenSSH
sudo ufw allow http
sudo ufw allow https
sudo ufw allow 25/tcp # if your server will support SMTP
Enable UFW
sudo ufw enable
Confirm your settings
sudo ufw status
Creating a Swapfile
Create a Swap File
# A good rule of thumb is to create a swapfile that is double your current memory.*
# E.g. for 1GB of memory, create a 2GB swapfile*
sudo fallocate -l 2G /swapfile
Make the swap file only accessible to root.
sudo chmod 600 /swapfile
Mark the file as a swap file.
sudo mkswap /swapfile
Tell the system to start using our new swap file,
sudo swapon /swapfile
Verify that the swap is now available
sudo swapon --show
Make the swapfile persist after reboots
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Server Dependencies
Add new repositories for NodeJS, Yarn and Redis
# Nodejs
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
# Yarn
curl -sL 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
# Redis (Optional)
sudo add-apt-repository ppa:chris-lea/redis-server
Refresh APT packages
sudo apt update
Install dependencies
# Remove redis-server and redis-tools if you are not using redis
sudo apt install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 \
libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev dirmngr gnupg apt-transport-https \
ca-certificates nodejs yarn redis-server redis-tools
Ruby
# Clone rbenv repo
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
# Add rbenv to $PATH
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
# Add rbenv init to .bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
# Add ruby-build plugin to rbenv
mkdir -p "$(rbenv root)"/plugins
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
# Install your ruby version
rbenv install 2.7.0
# Make your ruby version the default global version
rbenv global 2.7.0
# Skip installing gem documentation
echo "gem: --no-document" > ~/.gemrc
# Bundler
gem install bundler
PostgreSQL
Create the file /etc/apt/sources.list.d/pgdg.list and add a line for the repository
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main" >> /etc/apt/sources.list.d/pgdg.list'
Import the repository signing key, and update APT packages list
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
Install PostgreSQL
sudo apt install postgresql postgresql-contrib
Create a new user and database
# Switch to postgres user
sudo su - postgres
# Create user
# This should match database.yml
createuser -P deploy
# Create database
# This should match database.yml
createdb -O deploy myapp
# Exit postgres user
exit
Nginx
Install Nginx
sudo apt install nginx
Allow Nginx via UFW
sudo ufw allow 'Nginx FULL'
Systemd
Systemd is going to be used to manage our Puma server. This will allow us to manage Puma with Capistrano by sending start, stop, restart
.
Another benefit is that Systemd will try to restart our puma server if it goes down and will start automatically on a server reboot.
Create the following file: sudo nano /lib/systemd/system/puma.service
and add the following configuration
[Unit]
Description=Puma HTTP Server
After=network.target
[Service]
# Foreground process (do not use --daemon in ExecStart or config.rb)
Type=simple
# Preferably configure a non-privileged user
User=deploy
Group=deploy
# The path to the your app root directory.
WorkingDirectory=/home/deploy/myapp/current
ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec puma -C /home/deploy/myapp/shared/puma.rb'
ExecReload=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec pumactl -S /home/deploy/myapp/shared/tmp/pids/puma.state -F /home/deploy/myapp/shared/puma.rb restart'
ExecStop=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec pumactl -S /home/deploy/myapp/shared/tmp/pids/puma.state -F /home/deploy/myapp/shared/puma.rb stop'
Restart=always
[Install]
WantedBy=multi-user.target
Enable systemd puma service
sudo systemctl enable puma.service
Capistrano
Add capistrano to your Gemfile
# Gemfile
gem 'capistrano', '~> 3.12.0', require: false
gem 'capistrano-rbenv', require: false
gem 'capistrano-rails', require: false
gem 'capistrano-bundler', require: false
Generate capistrano files
bundle exec cap install STAGES=production
Update Capfile
# Capfile
require "capistrano/setup"
require "capistrano/deploy"
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git
require "capistrano/rbenv"
require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
Update config/deploy.rb
with the following
# config/deploy.rb
require File.expand_path("./environment", __dir__)
lock "~> 3.12.0"
set :rbenv_type, :user
set :rbenv_ruby, '2.7.0' # ruby version installed on your server
set :application, "myapp"
set :repo_url, "git@github.com:username/myapp.git"
set :deploy_to, "/home/deploy/#{fetch :application}"
set :format, :airbrussh
set :format_options, command_output: true, log_file: "log/capistrano.log", color: :auto, truncate: :auto
set :pty, false
# Add additional files that should be symlinked. For example files that are generally not checked into your repo
append :linked_files, 'config/database.yml'
# Remove 'public/packs', 'node_modules' if not using Webpacker
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', '.bundle', 'public/system', 'public/uploads', 'public/packs', 'node_modules'
set :keep_releases, 5
# Remove if not using Webpacker
set :assets_prefix, 'packs'
Update config/deploy/production.rb
with the following
# config/deploy/production.rb
server(server_ip, user: 'deploy', roles: %w{app db web})
set(:nginx_config_name, 'myapp_production')
Create lib/capistrano/tasks/puma.rake
namespace :puma do
task :start do
on roles(:app) do
execute :sudo, :systemctl, :start, :puma
end
end
task :stop do
on roles(:app) do
execute :sudo, :systemctl, :stop, :puma
end
end
task :restart do
on roles(:app) do
execute :sudo, :systemctl, :restart, :puma
end
end
end
after 'deploy:finished', 'puma:restart'
Create lib/capistrano/tasks/deploy.rake
namespace :deploy do
desc 'Upload application configuration files.'
task :app_config do
on roles(:app) do
execute "mkdir #{shared_path}/config/credentials -p"
upload! StringIO.new(File.read('config/database.yml')), "#{shared_path}/config/database.yml", via: :scp
end
end
desc "Upload nginx configuration"
task :nginx_config do
on roles :web do
upload! StringIO.new(File.read("config/puma/nginx.conf")),
"/tmp/#{fetch(:nginx_config_name)}"
execute :sudo, :mv,
"/tmp/#{fetch(:nginx_config_name)} /etc/nginx/sites-available/#{fetch(:nginx_config_name)}"
execute :sudo, :ln, "-fs",
"/etc/nginx/sites-available/#{fetch(:nginx_config_name)} /etc/nginx/sites-enabled/#{fetch(:nginx_config_name)}"
end
end
desc 'Upload puma config.'
task :puma_config do
on roles(:app) do
upload! StringIO.new(File.read('config/puma/production.rb')), "#{shared_path}/puma.rb", via: :scp
end
end
end
Puma and Nginx Configuration
Create config/puma/production.rb
and add:
directory '/home/deploy/myapp/current'
rackup "/home/deploy/myapp/current/config.ru"
environment 'production'
tag 'myapp'
pidfile "/home/deploy/myapp/shared/tmp/pids/puma.pid"
state_path "/home/deploy/myapp/shared/tmp/pids/puma.state"
stdout_redirect '/home/deploy/myapp/shared/log/puma_access.log', '/home/deploy/myapp/shared/log/puma_error.log', true
workers 1 # Change this to match the number of CPU cores your server has
threads 5,5 # This is matching the number of the connection pool for the DB. Feel free to tune this value.
bind 'unix:///home/deploy/myapp/shared/tmp/sockets/puma.sock'
restart_command 'bundle exec puma'
preload_app!
before_fork do
ActiveRecord::Base.connection_pool.disconnect!
end
on_worker_boot do
ActiveSupport.on_load(:active_record) do
ActiveRecord::Base.establish_connection
end
end
Create config/puma/nginx.conf
and add:
upstream puma_myapp_production {
server unix:/home/deploy/myapp/shared/tmp/sockets/puma.sock fail_timeout=0;
}
server {
server_name www.mydomain.com;
return 301 https://mydomain.com$request_uri;
}
server {
listen 80;
server_name mydomain.com;
return 301 https://$host$1$request_uri;
}
server {
listen 443;
ssl on;
ssl_certificate path/to/fullchain.pem;
ssl_certificate_key path/to/privkey.pem;
include path/to/options-ssl-nginx.conf;
ssl_dhparam path/to/ssl-dhparams.pem;
server_name mydomain.com;
root /home/deploy/myapp/current/public;
try_files $uri/index.html $uri @puma_myapp_production;
client_max_body_size 100M;
keepalive_timeout 10;
error_page 500 502 504 /500.html;
error_page 503 @503;
location @puma_myapp_production {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://puma_myapp_production;
# limit_req zone=one;
access_log /home/deploy/myapp/shared/log/nginx.access.log;
error_log /home/deploy/myapp/shared/log/nginx.error.log;
}
location ~ ^/(assets|packs)/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
location = /50x.html {
root html;
}
location = /404.html {
root html;
}
location @503 {
error_page 405 = /system/maintenance.html;
if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html break;
}
rewrite ^(.*)$ /503.html break;
}
if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){
return 405;
}
if (-f $document_root/system/maintenance.html) {
return 503;
}
}
Deployment
SSH onto your server and create the directory for your application.
mkdir /home/deploy/myapp
Delete the symlink file for the default nginx config
cd /etc/nginx/sites-enabled
rm default
At this point, we are ready to deploy our application.
Upload our application configuration files.
bundle exec cap production deploy:app_config
Upload the Nginx configuration.
bundle exec cap production deploy:nginx_config
Upload the Puma configuration.
bundle exec cap production deploy:puma_config
With the configuration files deployed, we can now deploy our application code
bundle exec cap production deploy