sajad torkamani

Note: This is a quickly hacked-together post that is mainly a reference for myself so probably not the best guide on the internet!


In this post, I'll outline the steps needed in order to deploy a Rails 6 application on a Ubuntu 18.04 server.

We'll need to setup and configure a few different tools:

Setup Ubuntu server

First, you want to make sure your Ubuntu server is setup correctly. Assuming a fresh server installation, you can follow this excellent article by Digital Ocean.

TLDR: Create a non-root sudo user, setup basic firewall, allow SSH access to this new user.

Install rbenv

rbenv is a Ruby package manager that makes it easy to use multiple Ruby versions on the same machine. This will be very handy for hosting multiple Rails projects on our server where different projects may require different Ruby versions. Refer to the Git installation method from the docs for installing on an Ubuntu server and also make sure to install the ruby-build plugin.

Install Nginx

Install a stable version of Nginx. Refer to the Nginx docs for installation instructions.

Install Phusion Passenger

Refer to the Passenger docs for installation instructions.

Setting up the Rails app

To prepare our Rails app for deployment, there's a couple of things we need to do.

Setup dotenv

Although Rails offers a way of managing secrets, I'm used to storing secrets in a .env file so I tend to use dotenv.

Create a .env.example file that lists all the required environment variables as placeholders and then create the .env file with the real values. An example .env file could be:

# Database credentials

# SMTP credentials

Install the dotenv-rails gem (same as dotenv but optimized for Rails usage) and create a config/initializers/dotenv.rb file to make all the keys in your .env.example file required (i.e. throw an error if any are not defined).

# Get all required env keys from .env.example
required_keys = Dotenv.parse('.env.example').keys

# Ensure all env vars are defined

Make sure to include .env in .gitgnore so that it's not added to version control.

Configure config/database.yml

Assuming a MySQL database, we can use the following database.yml file:

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: <%= ENV.fetch('DB_HOST') %>
  database: <%= ENV.fetch('DB_NAME') %>
  username: <%= ENV.fetch('DB_USERNAME') %>
  password: <%= ENV.fetch('DB_PASSWORD') %>

  <<: *default

  <<: *default
  database: myapp_test

  <<: *default

Check that your database credentials are setup properly by running rails db:create. If you don't get any errors, you're good to go!

Make sure to have a root path (Optional)

In order to test the app later, create a PagesController at app/controllers/pages_controller.rb :

class PagesController < ApplicationController
  def home

Then, create a corresponding view template at app/views/pages/home.html.erb :

<h1>Hello there!</h1>

Set this page as the home page by editing config/routes.rb:

Rails.application.routes.draw do
  root 'pages#home'

Install Capistrano

We'll use Capistrano - a remote server automation tool - to deploy our Rails apps on our remote Ubuntu server. Once Capistrano is set up correctly, we only have to run a command like cap <staging|production> deploy to deploy to the server. So all these steps will be worth it in the end!

Install Capistrano and friends

Add the core Capistrano gem as well as a couple of Capistrano-related gems to make things easier:

group :development do
  gem 'capistrano', '~> 3.14', require: false
  gem 'capistrano-passenger'
  gem 'capistrano-rails', '~> 1.6.1', require: false
  gem 'capistrano-rbenv', '~> 2.2.0'
  # other gems

Feel free to check the docs for each gem for more info.

Capify your project

Run bundle exec cap install and you should see an output like the following which tells us what files were generated:

mkdir -p config/deploy
create config/deploy.rb
create config/deploy/staging.rb
create config/deploy/production.rb
mkdir -p lib/capistrano/tasks
create Capfile

Edit config/deploy.rb:

lock '~> 3.14.1'

set :application, '<app_name>'
set :repo_url, '<username>/<repo>.git'
set :deploy_to, '/var/www/html/<app_name>'
set :rbenv_ruby,'.ruby-version').strip
set :keep_releases, 3
set :migration_role, :app

append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle',
       '.bundle', 'public/system', 'public/uploads'

# Just a helper task to see env variables on your server.
# Run with cap <environment> debug:env
namespace :debug do
  desc 'Print ENV variables'
  task :env do
    on roles(:web), in: :sequence, wait: 5 do
      execute :printenv

Edit config/deploy/production.rb :

server '<your_remote_server_ip>', user: '<ubuntu_username>', roles: %i[web app db]

after 'bundler:install', 'deploy:symlink_env_files' do
  on roles(:web) do
    within release_path do
      execute(:ln, '-s', '~/.env_files/<app_name>/.env', "#{release_path}/.env")

The deploy:symlink_env_files is how I personally manage environment variables on staging / production servers. I create an env file for each app under ~/.env_files and then just symlink that to .env in the release_path which is the folder that Capistrano deploys our app in.

You can do exactly the same for config/deploy/staging.rb if you have a staging environment. Just make sure to set the server and user values correctly.

Edit Capfile to require all the goodies:

require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/bundler'
require 'capistrano/rails'
require 'capistrano/passenger'
require 'capistrano/rbenv'

require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git

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

Create .env file on server

Create a .env file as ~/.env_files/<app_name>/.env. The ~ here should refer to the home directory of the deploy user set as user in config/deploy/production.rb.

Also, make sure to set a RAILS_MASTER_KEY environment variable in this .env file - this should match the one used to encrypt config/credentials.yml.env (use config/master.key if unsure).

Deploy the app

Make sure to create and publish your Git repo somewhere so Capistrano can pull from it on the remote server.

Then, run cap production deploy to attempt a deploy to your production environment.

The first attempt will likely take a while since a whole load of gems need to be installed. It should eventually fail and complain with something like ActiveRecord::NoDatabaseError: Unknown database <app_name_production>.

We can solve this pain in the butt by manually SSHing into the server, going inside any release folder of your deploy_to path (e.g. /var/www/html/<app_name>/releases/20201102172355) and running rails db:create to create the damn database.

Now, try cap production deploy again and you shouldn't see any errors. You should also see a current directory in your deploy_to path on the server. This directory contains your current release.

Configure Nginx & Passenger

The final step is to create our beautiful Nginx config file on the server at /etc/nginx/sites-available/

server {
  listen 80;
  listen [::]:80;

  root /var/www/html/myapp/current/public;

  passenger_enabled on;
  passenger_ruby /home/<your_username>/.rbenv/versions/<your_ruby_version>/bin/ruby;
  passenger_app_env production;

  location /cable {
    passenger_app_group_name myapp_websocket;
    passenger_force_max_concurrent_requests_per_process 0;

  # Allow uploads up to 100MB in size
  client_max_body_size 100m;

  location ~ ^/(assets|packs) {
    expires max;
    gzip_static on;

Check that the Nginx config is valid:

sudo nginx -t

Symlink the beauty:

ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/

Restart Nginx for the config to be picked up:

sudo service nginx restart

Make sure you set up the correct DNS records so that the server_name in your Nginx config actually points to your Ubuntu server.

Enjoy the bliss

Now, spin up your browser and navigate to the server_name URL you specified and you should see that your Rails app is available to the world.

Next, you'll probably want to add HTTPS support. What a pain...

Tagged: Rails