Deploying Rails application with Nginx, Puma and Mina

In this tutorial, I am going to show you how to setup your server to run Rails application on Nginx with Puma server, and how to deploy the app with Mina tool.

  1. Installing Ruby
    ====

First, we need to install some dependencies for Ruby

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 python-software-properties libmysqlclient-dev

Next, we are going to install Ruby using Ruby Version Manager (RVM). RVM allows us to quickly add/remove, upgrade/downgrade Ruby version without affecting other running applications on the server.

Note: The script for installing RVM is having issue with GPG, run the following command to download the key and import to your system first

curl -#LO https://rvm.io/mpapis.asc
gpg --import mpapis.asc

And then, start installing RVM as usual

sudo apt-get install libgdbm-dev libncurses5-dev automake libtool bison libffi-dev
curl -L https://get.rvm.io | bash -s stable
source ~/.rvm/scripts/rvm
echo "source ~/.rvm/scripts/rvm" >> ~/.bashrc
rvm install 2.1.2
rvm use 2.1.2 --default

Since Rails needs a JavaScript intepreter, we will need to install Node.js library as well

sudo apt-get install nodejs

We also need to install bundler

gem install bundler --no-ri --no-rdoc
  1. Setup directories & configure Mina
    ====

To make it easier to illustrate, I will use myapp as my application name, replace it with your own application name if you want.

First of all, create a directory in your server called /var/www/myapp, this is where the code is hosted. We also need to change the ownership of that directory to deploy user (or any user who you are using to deploy)

sudo mkdir -p /var/www/myapp
sudo chown deploy:deploy /var/www/myapp

Mina

Mina (http://nadarei.co/mina/) is a great tool for deployment and it is much faster than Capistrano. Previously, Capistrano was my first choice, but from now on, I think Mina will be my default deployment tool :).

It is very easy to setup Mina, you need to install it as a gem (no need to put it in Gemfile)

gem install mina

Then run

mina init

to generate config/deploy.rb file, which contains basic configuration of deployment.

Pay attention to some important lines in that file

set :user, 'deploy'
set :domain, 'myapp.com'
set :deploy_to, '/var/www/myapp'
set :app_path, lambda { "#{deploy_to}/#{current_path}" }
set :repository, 'git@github.com:example/example.git'
set :branch, 'master'
set :forward_agent, true

Next, run the following command at project location

mina setup

This will automatically log in to server and create two folders shared and releases inside /var/www/myapp directory.

Be sure to edit the content of database.yml and application.yml config file on server. They are located at /var/www/myapp/shared/config/ folder. If you are wondering what application.yml is used for, read this post: Gem Idol: Figaro

All the following steps assume that you have already updated the database.yml and application.yml with correct information. The database needs to be existed before continuting with later steps.

Lastly, start deployment process

mina deploy

If you get the error message Host key verification failed, try SSH into server and run the following command and try deploying again

ssh -T git@github.com #assuming your code is on Github, replace it with your Git server

After running mina deploy, there would be a new directory called current created in /var/www/myapp. This location will be used as the root directory when configuring nginx

  1. Installing Nginx
    ====
    I often install it via package manager

    sudo apt-get install nginx

After installing Nginx, we need to remove the default site

sudo rm /etc/nginx/conf.d/sites-enabled/default

Create a host config file at /etc/nginx/sites-available/my_app.conf

upstream my_app { 
	server unix:///var/www/myapp/shared/tmp/sockets/puma.sock; 
} 
server { 
	listen 80; 
	server_name myapp.com; # change to match your URL 
	root /var/www/myapp/current/public; # I assume your app is located at this location 

	location / { 
		proxy_pass http://my_app; # match the name of upstream directive which is defined above 
		proxy_set_header Host $host; 
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
	} 

	location ~* ^/assets/ { 
		# Per RFC2616 - 1 year maximum expiry 
		expires 1y; 
		add_header Cache-Control public; 

		# Some browsers still send conditional-GET requests if there's a 
		# Last-Modified header or an ETag header even if they haven't 
		# reached the expiry date sent in the Expires header. 
		add_header Last-Modified ""; 
		add_header ETag ""; 
		break; 
	} 
}

Pay attention to root and server_name, you need to set the correct path to public folder of your application for root, and use the correct URL for server_name

And then, we need to enable this site by creating a symlink in /etc/nginx/sites-enabled

sudo ln -sf /etc/nginx/sites-available/my_app.conf /etc/nginx/sites-enabled/my_app.conf

Finally, restart Nginx server

sudo service nginx restart
  1. Puma
    ====
    In order to run Puma on server, we are going to add two files: Puma configuration file and a shell script to restart Puma.

Create a new file at config/puma.rb with the following content

#!/usr/bin/env puma

environment ENV['RAILS_ENV'] || 'production'

daemonize true

pidfile "/var/www/myapp/shared/tmp/pids/puma.pid"
stdout_redirect "/var/www/myapp/shared/tmp/log/stdout", "/var/www/myapp/shared/tmp/log/stderr"

threads 0, 16

bind "unix:///var/www/myapp/shared/tmp/sockets/puma.sock"

Replace the path if necessary.

Next, create a file at bin/puma.sh with the below content

#! /bin/sh

PUMA_CONFIG_FILE=/var/www/myapp/current/config/puma.rb
PUMA_PID_FILE=/var/www/myapp/shared/tmp/pids/puma.pid
PUMA_SOCKET=/var/www/myapp/shared/tmp/sockets/puma.sock

# check if puma process is running
puma_is_running() {
  if [ -S $PUMA_SOCKET ] ; then
    if [ -e $PUMA_PID_FILE ] ; then
      if cat $PUMA_PID_FILE | xargs pgrep -P > /dev/null ; then
        return 0
      else
        echo "No puma process found"
      fi
    else
      echo "No puma pid file found"
    fi
  else
    echo "No puma socket found"
  fi

  return 1
}

case "$1" in
  start)
    echo "Starting puma..."
      rm -f $PUMA_SOCKET
      if [ -e $PUMA_CONFIG_FILE ] ; then
        bundle exec puma -C $PUMA_CONFIG_FILE
      else
        bundle exec puma
      fi

    echo "done"
    ;;

  stop)
    echo "Stopping puma..."
      kill -s SIGTERM `cat $PUMA_PID_FILE`
      rm -f $PUMA_PID_FILE
      rm -f $PUMA_SOCKET

    echo "done"
    ;;

  restart)
    if puma_is_running ; then
      echo "Hot-restarting puma..."
      kill -s SIGUSR2 `cat $PUMA_PID_FILE`

      echo "Doublechecking the process restart..."
      sleep 5
      if puma_is_running ; then
        echo "done"
        exit 0
      else
        echo "Puma restart failed :/"
      fi
    fi

    echo "Trying cold reboot"
    bin/puma.sh start
    ;;

  *)
    echo "Usage: script/puma.sh {start|stop|restart}" >&2
    ;;
esac

Finally, update Mina deploy.rb to include a new step which will be in charge of restarting Puma after deploying

desc "Deploys the current version to the server."
task :deploy => :environment do
  deploy do
    # Put things that will set up an empty directory into a fully set-up
    # instance of your project.
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile'
    invoke :'deploy:cleanup'

    to :launch do
      invoke :'puma:restart'
    end
  end
end

namespace :puma do
  desc "Start the application"
  task :start do
    queue 'echo "-----> Start Puma"'
    queue "cd #{app_path} && RAILS_ENV=#{stage} && bin/puma.sh start", :pty => false
  end

  desc "Stop the application"
  task :stop do
    queue 'echo "-----> Stop Puma"'
    queue "cd #{app_path} && RAILS_ENV=#{stage} && bin/puma.sh stop"
  end

  desc "Restart the application"
  task :restart do
    queue 'echo "-----> Restart Puma"'
    queue "cd #{app_path} && RAILS_ENV=#{stage} && bin/puma.sh restart"
  end
end

Important note:

You have to add execute permission to puma.sh file to make sure it could be executed on the server

chmod +x bin/puma.sh

And be sure to check puma.sh and puma.rb into the remote repository before deploying.

After configuring all step above, we should be able to deploy the application by running the following command in the project folder

mina deploy

References

This tutorial contains material from several other blog posts, I recommend reading them to further enhance your knowledge on these things

  1. How to setup Rails app with Puma and Nginx
  2. Deploy Ruby on Rails on Ubuntu 14.04 Trusty Tahr
  3. Gist with the code for deploying Rails on Nginx + Puma using Mina
  4. How To Use Mina to Deploy a Ruby on Rails Application

That's all, if you have any issue when setting up the server with these steps, leave a comment and I will be ready to help