Introduction

We will be setting up a Ruby on Rails production environment on Ubuntu 18.04 LTS Bionic Beaver.

Since we setup Ubuntu for our development environment, we also want to use it in production. This keeps your application running consistently between development and production. We’re using an LTS version of Ubuntu in production because it is supported for several years where a normal version of Ubuntu isn’t.

First you need a clean system Ubuntu 18.04 Bionic Bever Server.

Also replace Ubuntu 18.04 Bionic default /etc/apt/sources.list for the next list.

sudo vim /etc/apt/sources.list

Content (need check this):

#deb cdrom:[Ubuntu 18.04 LTS _Bionic Beaver_ - Release amd64 (20180426)]/ bionic main restricted

# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
# newer versions of the distribution.
deb http://us.archive.ubuntu.com/ubuntu/ bionic main restricted
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic main restricted

## Major bug fix updates produced after the final release of the
## distribution.
deb http://us.archive.ubuntu.com/ubuntu/ bionic-updates main restricted
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic-updates main restricted

## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu
## team. Also, please note that software in universe WILL NOT receive any
## review or updates from the Ubuntu security team.
deb http://us.archive.ubuntu.com/ubuntu/ bionic universe
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic universe
deb http://us.archive.ubuntu.com/ubuntu/ bionic-updates universe
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic-updates universe

## N.B. software from this repository is ENTIRELY UNSUPPORTED by the Ubuntu
## team, and may not be under a free licence. Please satisfy yourself as to
## your rights to use the software. Also, please note that software in
## multiverse WILL NOT receive any review or updates from the Ubuntu
## security team.
deb http://us.archive.ubuntu.com/ubuntu/ bionic multiverse
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic multiverse
deb http://us.archive.ubuntu.com/ubuntu/ bionic-updates multiverse
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic-updates multiverse

## N.B. software from this repository may not have been tested as
## extensively as that contained in the main release, although it includes
## newer versions of some applications which may provide useful features.
## Also, please note that software in backports WILL NOT receive any review
## or updates from the Ubuntu security team.
deb http://us.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse
# deb-src http://us.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse

## Uncomment the following two lines to add software from Canonical's
## 'partner' repository.
## This software is not part of Ubuntu, but is offered by Canonical and the
## respective vendors as a service to Ubuntu users.
# deb http://archive.canonical.com/ubuntu bionic partner
# deb-src http://archive.canonical.com/ubuntu bionic partner

deb http://security.ubuntu.com/ubuntu bionic-security main restricted
# deb-src http://security.ubuntu.com/ubuntu bionic-security main restricted
deb http://security.ubuntu.com/ubuntu bionic-security universe
# deb-src http://security.ubuntu.com/ubuntu bionic-security universe
deb http://security.ubuntu.com/ubuntu bionic-security multiverse
# deb-src http://security.ubuntu.com/ubuntu bionic-security multiverse

Perform a system update.

sudo apt-get update
sudo apt-get upgrade

The first thing we will do on our new server is create the user account we’ll be using to run our applications and work from there.

sudo adduser deploy
sudo adduser deploy sudo
su deploy

Config run sudo command without a password

Backup your /etc/sudoers file by typing the following command:

sudo cp /etc/sudoers /root/sudoers.bak

Edit the /etc/sudoers file by typing the visudo command:

sudo visudo

Append the following entry to run ALL command without a password for a user named deploy at the end of the file:

deploy ALL=(ALL) NOPASSWD:ALL

Save and close the file. Now you can run any command as root user:

sudo systemctl restart nginx
sudo reboot
sudo apt-get install htop

## get root shell ##
sudo -i

Config ssh access via ssh-key

Before we move forward is that we’re going to setup SSH to authenticate via keys instead of having to use a password to login. It’s more secure and will save you time in the long run.

We’re going to use ssh-copy-id to do this. If you’re on OSX you may need to run brew install ssh-copy-id but if you’re following this tutorial on Linux desktop, you should already have it.

Once you’ve got ssh-copy-id installed, run the following and replace IPADDRESS with the one for your server:

Make sure you run ssh-copy-id on your computer, and NOT the server.

ssh-copy-id deploy@IPADDRESS

Now when you run ssh deploy@IPADDRESS you will be logged in automatically. Go ahead and SSH again and verify that it doesn’t ask for your password before moving onto the next step.

For the next steps, make sure you are logged in as the deploy user on the server!

Installing Ruby

The first step is to install some dependencies for Ruby and Rails.

To make sure we have everything necessary for Webpacker support in Rails, we’re first going to start by adding the Node.js and Yarn repositories to our system before installing them.

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
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 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 nodejs yarn

Next we’re going to be installing Ruby using one of three methods. Each have their own benefits, most people prefer using rbenv these days, but if you’re familiar with rvm you can follow those steps as well. I’ve included instructions for installing from source as well, but in general, you’ll want to choose either rbenv or rvm.

Choose one method. Some of these conflict with each other, so choose the one that sounds the most interesting to you, or go with my suggestion, rbenv.

Installing with rbenv is a simple two step process. First you install rbenv, and then ruby-build:

cd
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL

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

rbenv install 2.6.1
rbenv global 2.6.1
ruby -v

The last step is to install Bundler

gem install bundler

rbenv users need to run rbenv rehash after installing bundler.

Installing Nginx

For our setup, we’ll be using NGINX as our webserver to receive HTTP requests. Those requests will then be handed over to Passenger which will run our Ruby app.

Phusion is the company that develops Passenger and they recently put out an official Ubuntu package that ships with Nginx and Passenger pre-installed.

We’ll be using that to setup our production server because it’s very easy to setup.

sudo apt-get install -y dirmngr gnupg
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates

sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bionic main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update

sudo apt-get install -y nginx-extras libnginx-mod-http-passenger

if [ ! -f /etc/nginx/modules-enabled/50-mod-http-passenger.conf ]; then sudo ln -s /usr/share/nginx/modules-available/mod-http-passenger.load /etc/nginx/modules-enabled/50-mod-http-passenger.conf ; fi
sudo ls /etc/nginx/conf.d/mod-http-passenger.conf

So now we have Nginx and passenger installed. We can manage the Nginx webserver by using the systemd:

sudo systemctl start nginx

To check and make sure your Passenger install is configured correctly, run the following command:

sudo /usr/bin/passenger-config validate-install

Open up the server’s IP address in your browser to make sure that nginx is up and running.

The systemctl command also provides some other methods such as restart and stop that allow you to easily restart and stop your webserver.

Next, we need to update the Nginx configuration to point Passenger to the version of Ruby that we’re using. You’ll want to open up /etc/nginx/conf.d/mod-http-passenger.conf in your favorite editor. I like to use vim, so I’d run this command:

sudo vim /etc/nginx/conf.d/mod-http-passenger.conf

# You could also use nano if you don't like vim
# sudo nano /etc/nginx/conf.d/mod-http-passenger.conf

And change the passenger_ruby line to point to your ruby executable:

passenger_ruby /home/deploy/.rbenv/shims/ruby; # If you use rbenv
# passenger_ruby /home/deploy/.rvm/wrappers/ruby-2.1.2/ruby; # If use use rvm, be sure to change the version number
# passenger_ruby /usr/bin/ruby; # If you use ruby from source

The passenger_ruby is the important line here. Make sure you only set this once and use the line from the example that pertains to the version of Ruby you installed.

Once you’ve changed passenger_ruby to use the right version Ruby, you can run the following command to restart Nginx with the new Passenger configuration.

sudo systemctl restart nginx

Now that we’ve restarted Nginx, the Rails application will be served up using the deploy user just how we want. In the Capistrano section we will talk about configuring Nginx to serve up your Rails application.

Installing PostgreSQL

Postgres 10.5 is available in the Ubuntu repositories and we can install it like so:

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

Next we need to setup our postgres user (also named “deploy” but different from our linux user named “deploy”) and database:

sudo su - postgres
createuser --pwprompt deploy
createdb -O deploy my_app_name_production # change "my_app_name" to your app's name which we'll also use later on
exit

The password you type in here will be the one to put in your my_app/current/config/database.yml later when you deploy your app for the first time.

Capistrano Setup

For Capistrano, make sure you do these steps on your development machine inside your Rails app.

Capistrano is a Ruby library that we’ll use to deploy our code to our production server. It will maintain a copy of our git repo on the server and a set of release folders. Each release is a copy of our app whenever it was deployed and we’ll have a current symlink that will be what’s running in production. That will point to which release is currently running and allow us to easily rollback to previous releases should something go wrong.

The first step is to add Capistrano to your Gemfile:

gem 'capistrano'
gem 'capistrano-rails'
gem 'capistrano-passenger'
gem 'capistrano-rbenv'
gem 'capistrano-bundler'

Once these are added, run the following command to generate your capistrano configuration.

cap install STAGES=production

Next we need to make some additions to our Capfile to include bundler, rails, and rbenv. Edit your Capfile and add these lines:

require 'capistrano/rails'
require 'capistrano/passenger'
require 'capistrano/rbenv'
require "capistrano/bundler"
set :rbenv_type, :user
set :rbenv_ruby, '2.6.1'

After we’ve got Capistrano installed, we can configure the config/deploy.rb to setup our general configuration for our app. Edit that file and make it like the following replacing my_app_name with the name of your application and git repository:

set :application, 'my_app_name'
set :repo_url, 'git@example.com:me/my_repo.git'

set :deploy_to, '/home/deploy/my_app_name'

append :linked_files, 'config/database.yml', 'config/master.key'
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads'

Now we need to open up our config/deploy/production.rb file to set the server IP address that we want to deploy to:

# Replace 127.0.0.1 with your server's IP address!
server '127.0.0.1', user: 'deploy', roles: %w{app db web}

If you have any trouble with Capistrano or the extensions for it, check out Capistrano’s Github page.

Final Steps

Thankfully there aren’t a whole lot of things to do left!

Adding The Nginx Host

In order to get Nginx to respond with the Rails app, we need to modify it’s sites-enabled.

Open up /etc/nginx/sites-available/production.my_app.host in your text editor and we will replace the file’s contents with the following:

server {
        listen 80;
        listen [::]:80 ipv6only=on;
        
        server_name production.my_app.host;
        passenger_enabled on;
        rails_env    production;
        root         /home/deploy/my_app/current/public;

        # redirect server error pages to the static page /50x.html
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
}

This is our Nginx configuration for a server listening on port 80. You need to change the server_name values to match the domain you want to use and in root replace my_app_name with the name of your application.

Next created link on site config:

sudo ln -s /etc/nginx/sites-available/production.my_app.host /etc/nginx/sites-enabled/
sudo systemctl restart nginx

Install Certbot for https certificate

Need before configuring domain name.

sudo add-apt-repository ppa:certbot/certbot
sudo apt-get install python-certbot-nginx
sudo certbot --nginx

Connecting The Database

One optional thing I would recommend is to remove your config/database.yml and config/master.key git and only store example copies in your git repo. This way we can easily copy the files for setting up development, but our production environment can symlink files on the server so that our production secrets and passwords are only stored on the production server.

First we’ll move these files to their example names in the git repo.

echo "config/database.yml\nconfig/master.key" >> .gitignore
git add .gitignore
git mv config/database.yml config/database.yml.example
git commit -m "Only store example secrets and database configs"
cp config/database.yml.example config/database.yml

You can run cap production deploy to deploy your application, but it’s going to fail this first time because we haven’t created either of these files on the server which we will do in just a second.

linked file /home/deploy/my_app_name/shared/config/database.yml does not exist on IP_ADDRESS

One last time, ssh into your server as the deploy user and this time we need to create two files. First is the database.yml that uses the password for the postgres user you created earlier.

# /home/deploy/my_app_name/shared/config/database.yml
production:
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: <%= ENV.fetch('PGHOST') %>
  user: <%= ENV.fetch('PGUSER') %>
  password: <%= ENV.fetch('PGPASS') %>
  timeout: 5000
  database: <%= ENV.fetch('PGDATABASE') %>

To do this, let’s skip it on ssh

scp config/database.yml deploy@IP_ADDRESS:/home/deploy/my_app_name/shared/config/database.yml

Next, we need to copy the master.key

scp config/master.key deploy@192.168.88.244:/home/deploy/my_app_name/shared/config/master.key

We also need to store the environment variables, for this purpose I chose the dotenv-rails gem and the file name .env.production. The next team will copy it the same:

scp .env.production deploy@192.168.88.244:/home/deploy/my_app_name/shared/.env

You can run cap production deploy one last time to get your full deployment to run. This should completed successfully and you should see your new site live! You can just run Capistrano again to deploy any new changes you’ve pushed up to your Git repository.

Restarting The Site

One last thing you should know is that restarting just the Rails application with Passenger is very easy. If you ssh into the server, you can run touch my_app_name/current/tmp/restart.txt or sudo passenger-config restart-app and Passenger will restart the application for you. It monitors the file’s timestamp to determine if it should restart the app. This is helpful when you want to restart the app manually without deploying it.

Conclusion

And there you have it, a very long-winded explanation of all the different things you need to do while setting up an application to be deployed. There is a lot of system administration pieces that can expand upon this, but that’s for another time.