Jun 21, 2017

Deploying Ruby on Rails app to a VPS

blogpost author photo
Adam Guderski
blogpost cover image
This short guide covers all steps you should take in order to deploy your Ruby on Rails application to a VPS (Virtual Private Server) and make it available to everyone over the Internet. 

I assume that you already have ordered a VPS from your hosting provider. For the easiest configuration, I suggest you use Ubuntu Server 16.04 as an operating system.

Package installation

First, log in to your server using SSH as root (or as another user and then switch to root with sudo -i or su -l) and upgrade your system:

apt-get update -qqy && apt-get dist-upgrade -qqy

The option -qq means "quiet", while -y answers "yes" to all questions.

Then you can proceed with installation of the necessary packages. We will be using Ruby Version Manager (RVM), so additional packages for building Ruby are needed as well. I assume that your application uses PostgreSQL as the database engine. The list below probably includes packages that your application does not need, but unless you really need to save disk space then you are good to go and install all of them:

apt-get install -qqy git curl nodejs libpq-dev imagemagick libmagickwand-dev nginx openssl ssl-cert libssl-dev ca-certificates postgresql make automake autoconf libc6-dev patch libreadline6 libreadline6-dev zlib1g zlib1g-dev libyaml-dev libsqlite3-dev sqlite3 libgmp-dev libgdbm-dev libncurses5-dev libtool bison pkg-config libffi-dev

Preparing a user account for the application

For security reasons, your application should use a separate user account. Let's prepare one - here for example, the username will be ruby-app:

adduser --home /home/ruby-app \
    --disabled-password \
    --shell /bin/bash \
    --gecos "ruby-app" \

The created user account will use Bash shell (--shell /bin/bash), be described as "ruby-app" (--gecos "ruby-app") and have no password (--disabled-password) so you will need to enable logging in as this user with an SSH key:

su - ruby-app
mkdir ~/.ssh/
touch ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Now open the file /home/ruby-app/.ssh/authorized_keys with your favorite text editor and paste in your public SSH key. Staying logged in as the ruby-app user, execute the following commands to install RVM:

gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
\curl -sSL https://get.rvm.io | bash -s stable
echo bundler >> ~/.rvm/gemsets/global.gems
echo -e "rvm_install_on_use_flag=1\nrvm_gemset_create_on_use_flag=1\nrvm_quiet_curl_flag=1" > ~/.rvmrc

Okay, now let's create a directory structure where your application will reside:

mkdir -p ~/www/{releases,shared}
mkdir -p ~/www/shared/{config,log,sockets,tmp,public}
mkdir -p ~/www/shared/tmp/pids
chmod 700 ~/www/shared/config

Since RoR apps usually make use of environment variables, it is wise to keep them in a single file, for example, named app.env. Let's create such file, secure its permissions, fill it with basic variables and source it in ~/.bashrc so that its variables are available upon login:

touch ~/app.env
chmod 600 ~/app.env
echo -e "RAILS_ROOT='\$HOME/www/current'\nRAILS_ENV=production\nSTART_CMD='bundle exec unicorn_rails -c config/unicorn.rb -E $RAILS_ENV -D'"> ~/app.env
sed -i '5i set -a; source \$HOME/app.env; set +a\n' ~/.bashrc

Configuring application server

In this guide, our Rails app uses unicorn as the application server, so we should create a basic configuration file for it - create a file ~/www/shared/config/unicorn.rb with the following contents:

worker_processes 1
working_directory "/home/ruby-app/www/current"
listen "/home/ruby-app/www/shared/sockets/unicorn.sock", :backlog => 64
timeout 30
pid "/home/ruby-app/www/shared/tmp/pids/unicorn.pid"
stderr_path "/home/ruby-app/www/shared/log/unicorn.stderr.log"
stdout_path "/home/ruby-app/www/shared/log/unicorn.stdout.log"

preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
  GC.copy_on_write_friendly = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and

   old_pid = "#{server.config[:pid]}.oldbin"
   if old_pid != server.pid
       sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
       Process.kill(sig, File.read(old_pid).to_i)
     rescue Errno::ENOENT, Errno::ESRCH

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and

Most of the above lines are self-explanatory - you can read about them in the documentation.

Configuring systemd service

We would like for our application to start automatically after our VPS boots, wouldn't we? In case our server is restarted (accidentally or on purpose), we won't need to log in to the server and start it manually. Modern Linux systems use a system manager called systemd - it is possible to create our own service and control it with systemd.

First, we need to create a service file - create a file /etc/systemd/system/ruby-app.service as root and fill it with the following:

Description=Unicorn server for ruby-app

ExecStart=/bin/bash -alc "cd $RAILS_ROOT && $START_CMD "
ExecReload=/bin/kill -USR2 $MAINPID


Don't forget to execute systemctl daemon-reload to make this service available to systemd. To autostart this service on system boot, execute systemctl enable ruby-app.service.

To control systemd services, root privileges are required. Let's make it possible to start/stop/restart/reload our service by ruby-app user with sudo. Create a file `/etc/sudoers.d/ruby-app with the following contents:

ruby-app ALL=(ALL)NOPASSWD:/usr/bin/systemctl start ruby-app.service
ruby-app ALL=(ALL)NOPASSWD:/usr/bin/systemctl stop ruby-app.service
ruby-app ALL=(ALL)NOPASSWD:/usr/bin/systemctl restart ruby-app.service
ruby-app ALL=(ALL)NOPASSWD:/usr/bin/systemctl reload ruby-app.service

Setting up PostgreSQL

To create PostgreSQL database and user for your application, execute the following as root. Replace PASSWORD with strong, randomly generated password.

su - postgres -c psql
create role 'ruby-app' with login password PASSWORD;
create database 'ruby-app_production' owner 'ruby-app` encoding UTF8 template template0;

Then, as ruby-app user, create a file ~/www/shared/config/database.yml with the following contents:

  adapter: postgresql
  encoding: unicode
  pool: 10
  host: localhost
  database: ruby-app_production
  username: ruby-app
  password: PASSWORD

Configuring nginx

We will use nginx as HTTP reverse proxy, so that all static files are handled by nginx while the application by unicorn. Using nginx as reverse proxy also allows us to make our application available over HTTPS.

Create a file /etc/nginx/sites-available/ruby-app.conf with the following contents - of course, replace all occurrences of your-domain.com with the domain name that points to your server:

upstream ruby-app-unicorn {
    server unix:/home/ruby-app/www/shared/sockets/unicorn.sock fail_timeout=0;

server {
    listen 80;

    server_name your-domain.com;

    access_log /var/log/nginx/your-domain.com.access.log;
    error_log /var/log/nginx/your-domain.com.error.log;

    root /home/ruby-app/www/current/public;

    try_files $uri/index.html $uri @unicorn;

    location @unicorn {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;ruby-app
        proxy_pass http://DOMAIN-unicorn;
        proxy_connect_timeout 600;
        proxy_send_timeout 600;
        proxy_read_timeout 600;
        proxy_set_header X-Forwarded-Proto $scheme;

Then you make this site definition enabled and reload nginx:

ln -s /etc/nginx/sites-available/ruby-app.conf /etc/nginx/sites-enabled/ruby-app.conf
systemctl reload nginx.service

Now, browsing to http://your-domain.com/ should result in 502 Bad gateway error - it means that unicorn is not started yet.

Your first deploy with Capistrano

Next steps should be performed locally, in the directory where your code is located. First, install Capistrano:

gem install capistrano
gem install capistrano-rvm
cap install

This will create a set of files necessary for deployment. There are some changes that need to be done:

  1. config/deploy.rb:

    • change application to whatever you like
    • set repo_url to git repo where your code is available
    • set deploy_to to /home/ruby-app/www/
    • add line set :linked_files, %w(config/database.yml config/unicorn.rb)
    • add line set :linked_dirs, %w(log vendor/bundle tmp/sockets tmp/pids tmp/cache)
  2. config/deploy/production.rb - replace everything with the following (in place of SERVER_HOSTNAME put the hostname or IP address of your VPS):

role :app,        %w(SERVER_HOSTNAME)
role :web,        %w(SERVER_HOSTNAME)
role :db,         %w(SERVER_HOSTNAME), primary: true
set :application, 'ruby-app'

server 'SERVER_HOSTNAME', user: fetch(:application), roles: %w(web app db), primary: true

set :full_app_name, 'ruby-app'
set :rails_env,   'production'

namespace :deploy do
  desc 'Restart application'
  task :stop do
    on roles(:app), in: :sequence, wait: 10 do
      # command that is used to stop Unicorn goes here
      execute 'sudo systemctl stop ruby-app.service'

  task :start do
    on roles(:app), in: :sequence, wait: 10 do
      # command that is used to start Unicorn goes here
      execute 'sudo systemctl start ruby-app.service'

  after :publishing, :stop
  after :stop, :start

Now you can commit and push all your changes, and after that you can execute cap production deploy to deploy you application to your VPS. After Capistrano finishes successfully, navigating to http://your-domain.com should produce the main page of your application, now available to everyone on the Internet.