Rails Deployment Tutorial

This tutorial shows how to deploy Ruby on Rails applications. It is tested and known to work on:

Deploying a new version will be fully automated and only take one shell command:

Rails deploy to server

The tutorial establishes naming conventions and a folder structure suitable for deploying multiple Rails applications on the same server:

Deploy Rails app

Using different Ruby or Rails versions is supported, so you can upgrade a single application to the latest Rails or use the latest Rails for a new application without having to upgrade all the other apps at the same time.

Setting everything up takes about 2 hours. Once you are familiar with the process it takes about 15 minutes to add another application to the server, and seconds to deploy a new version.

The tutorial makes the assumption that the applications run on only one server, that the server is used only for hosting Rails applications and that users that have shell access to the server are trusted.

Tools

Requirements

For this tutorial you need to have good knowledge of Linux server administration using a command line shell. You need to know how to version a project using Git.

USE THIS ARTICLE AT YOUR OWN RISK: None of the authors, in any way whatsoever, can be responsible for your use of the information contained in these web pages. Do not rely upon any information found here without independent verification.

Demo app

Fixing server issues and app-specific issues at the same time can be frustrating. So get a simple application running first, then deploy your Rails app. I recommend creating a demo app yourself (alternatively use rails-demo):

  1. Check your Ruby and Rails versions:

    $ ruby --version
    ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
    $ gem list rails
    rails (5.1.4)

    If you don’t have the latest versions, install rvm according to the instructions on their website and then install the latest Ruby on Rails:

    rvm install 2.4.2
    gem update --system
    gem install rails bundler
  2. Create a new app:

    rails new demo
  3. Set the ruby version that you want to use in .ruby-version:

    cd demo/
    echo "2.4.2" > .ruby-version
  4. Edit the Gemfile and add a production group with the gems pg (PostgreSQL) and unicorn (the Rack HTTP server):

    group :production do
      gem 'pg'
      gem 'unicorn'
    end

    If you like, you can also use the unicorn server during development by declaring the dependency outside of the :production group.

    You could optionally wrap the sqlite3 gem in a group for development and test (if you use it to test on your development machine) so that it doesn’t get installed in production:

    group :development, :test do
      gem 'sqlite3' 
    end
  5. Update Gemfile.lock by installing all dependencies locally:

    bundle install --without=production

    The parameter --without=production is only needed when bundle install is run the first time, after that Bundler remembers the setting in .bundle/config.

  6. Generate a controller for the index page and a model:

    bin/rails generate controller welcome index
    bin/rails generate model counter name:string value:integer
  7. Edit config/routes.rb and set the welcome controller as root route:

    Rails.application.routes.draw do
      root 'welcome#index'
    
      # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
    end
  8. Edit app/controllers/welcome_controller.rb and make it use the model object:

    class WelcomeController < ApplicationController
      def index
        counter = Counter.find_or_create_by(name: "default")
        counter.value = (counter.value || 0) + 1
        counter.save!
        render plain: "Counter #{counter.value}; #{Rails.version}/#{RUBY_VERSION}"
      end
    end
  9. Migrate the database and run the application locally:

    bin/rake db:migrate
    bin/rails s
  10. Check that the app works:

  11. Create a local git repository and commit the app:

    git init
    git add .
    git commit -m "demo project"
  12. Create a Git repository on Bitbucket and push the project to the repository, for example:

    git remote add origin git@bitbucket.org:example/rails-demo.git
    git push -u origin --all

VM setup

  1. Sign-up at DigitalOcean (if you use this link, you’ll get $10 in credit so you can try out a VM for free).

  2. Create a new droplet.

  3. Select Ubuntu 16.04.3 x64 or Debian 9.2 x64 as Image (don’t use the One-click apps for this tutorial).
    Select the size for your droplet (you can increase the CPU and RAM later, but you cannot downgrade to a size with less disk space):

    Create a DigitalOcean droplet
  4. It’s highly recommended to use an SSH key for logging in to your server without a password:

    Add SSH Key

    If you don’t have used SSH keys before, check out the guide How To Use SSH Keys with DigitalOcean Droplets. You can create a key pair and get the public key with:

    ssh-keygen
    cat ~/.ssh/id_rsa.pub
  5. Create the Droplet and wait for the VM to be created.

  6. Log-in to the system:

    ssh root@serverip

Linux system setup

  1. Set the timezone:

    dpkg-reconfigure tzdata
  2. Update all packages and reboot:

    apt-get update && apt-get upgrade && apt-get autoremove && reboot
  3. Check the open ports (should be only SSH):

    netstat --listening --tcp
  4. Enable the firewall so that unconfigured services will not be exposed:

    ufw allow 22 && ufw logging off && ufw enable && ufw status

    On Debian you need to install the ufw (uncomplicated firewall) package first, on Ubuntu it’s installed by default:

    apt-get install ufw

    The firewall rules are automatically saved and restored on reboot.

PostgreSQL setup

  1. Install PostgreSQL:

    apt-get install postgresql libpq-dev
  2. Create a user and a database for the application:

    sudo -u postgres createuser rails-demo
    sudo -u postgres createdb rails-demo --owner=rails-demo

    (You can ignore the “could not change directory to "/root": Permission denied” warnings)

    On local connections, PostgreSQL uses the peer authentication method that obtains the client’s user name from the operating system and uses it as database user name. So if you’re logged in as the app user rails-demo, you’re allowed to access the rails-demo database using the psql client.

Ruby on Rails setup

  1. Install git amd nodejs (the Rails asset pipeline needs a JavaScript interpreter to minify JavaScript files):

    apt-get install git nodejs

    On Ubuntu also install rng-tools (which is required to initialize Linux’s pool of random numbers quickly - otherwise after rebooting it will take minutes until Ruby programs can be run):

    apt-get install rng-tools

    On Debian, also install these packages which are required for the installation of rvm but are not installed by default:

    apt-get install curl dirmngr
  2. Install Ruby Version Manager (RVM) according to the instructions on their website.

  3. Log-out and log-in to enable rvm.

  4. Install Ruby and the bundler gem:

    rvm install 2.4.2 && rvm --default use 2.4.2 && gem update --system && gem install bundler
  5. Create a user for the app:

    APP_NAME=rails-demo
    adduser $APP_NAME --disabled-password
  6. Copy your SSH public key to the user home so you can log-in as the app user, for example:

    mkdir /home/$APP_NAME/.ssh
    cp ~/.ssh/authorized_keys /home/$APP_NAME/.ssh/
    chown $APP_NAME.$APP_NAME /home/$APP_NAME/.ssh -R
    chmod go-rwx /home/$APP_NAME/.ssh -R
  7. Log-out and log-in as the app user:

    ssh rails-demo@serverip
  8. Generate a SSH key pair without password as deployment key:

    ssh-keygen && cat ~/.ssh/id_rsa.pub
  9. Add the deployment key to the repository:

    Add the deployment key to the repository

Setting up the project for deployment with Mina

  1. On your local machine, install Mina:

    cd demo/
    gem install mina -v 1.2.2

    (this tutorial was tested with Mina version 1.2.2 - if there are newer versions available, it’s recommended to use 1.2.2 first and then update to the new version).

  2. Add a config/deploy.rb configuration file to the Rails project (example configuration):

    curl https://bitbucket.org/rebert/rails-deploy/raw/master/example-deploy.rb > config/deploy.rb

    Change the option :domain to the server ip and :repository to the clone URL of the Bitbucket repository:

    nano config/deploy.rb

    The example configuration for Mina has been customized for this tutorial to set up rvm according to .ruby-version and to use service ... restart for launching after deployment. It also automatically creates a database.yml and secrets.yml configuration file on the server during mina setup.

  3. Let mina create the app folder structure on the server:

    mina setup
  4. On the server, if needed, edit the created configuration files:

    nano app/shared/config/database.yml
    nano app/shared/config/secrets.yml
  5. To deploy the app to the server, run locally:

    mina deploy

    This should get the project from the repository, install all gems, update the database, create the assets and finish like this:

    ...
    -----> Launching        
    -----> Done. Deployed v1        
           Elapsed time: 168.77 seconds

Running unicorn

  1. Log-in to the server forwarding a local port to a server port:

    ssh -L 4000:localhost:4000 rails-demo@serverip
  2. Create a unicorn configuration file on the server:

    curl https://bitbucket.org/rebert/rails-deploy/raw/master/example-unicorn > ~/app/shared/config/unicorn.conf.rb
    nano ~/app/shared/config/unicorn.conf.rb

    Example configuration:

    app_path = File.expand_path(File.join(File.dirname(__FILE__), '../../'))
    
    listen '127.0.0.1:4000'
    listen File.join(app_path, 'shared/unicorn.sock'), :backlog => 64
    
    worker_processes 2
    
    working_directory File.join(app_path, 'current')
    pid File.join(app_path, 'shared/unicorn.pid')
    stderr_path File.join(app_path, 'current/log/unicorn.log')
    stdout_path File.join(app_path, 'current/log/unicorn.log')
  3. Run the unicorn Rails server:

    cd ~/app/current
    RAILS_ENV=production bundle exec unicorn -c ~/app/shared/config/unicorn.conf.rb
  4. Open http://localhost:4000/ to check if the Rails application is running on the server as expected.

    Otherwise, check the application log files:

    tail -f ~/app/shared/log/*

Unicorn service / init.d script

  1. Log-in as root on the server side and create a init.d-script for unicorn (example script):

    curl https://bitbucket.org/rebert/rails-deploy/raw/master/example-initd > /etc/init.d/rails-demo
  2. Customize the script: This script was derived from /etc/skeleton and is made for the conventions used in this tutorial - if you used all the same paths, the only thing you need to change is APP_NAME. Special feature: unicorn is started under the app user account and the unicorn / rvm path is detected automatically according to the .ruby-version project file.

    nano /etc/init.d/rails-demo
  3. Make the script executable:

    chmod +x /etc/init.d/rails-demo
  4. Enable the service:

    update-rc.d rails-demo defaults
  5. Test starting and stopping the server:

    service rails-demo start
    service rails-demo status
    service rails-demo restart
  6. Check the Rails application log files and test the app using SSH port forwarding:

    tail /home/rails-demo/app/shared/log/*
  7. Test that the service comes up after a system reboot:

    reboot

Web server setup

The unicorn server is not made to serve HTTP requests directly, so let’s put a nginx web server in front of it:

  1. Log-in as root again:

    ssh root@serverip
  2. Install nginx :

    apt-get install nginx
  3. Disable the default page:

    rm /etc/nginx/sites-enabled/default
  4. Create a nginx site for the application:

    curl https://bitbucket.org/rebert/rails-deploy/raw/master/example-nginx-site > /etc/nginx/sites-available/rails-demo
    nano /etc/nginx/sites-available/rails-demo

    Example configuration:

    upstream rails-demo {
    	server unix:/home/rails-demo/app/shared/unicorn.sock fail_timeout=0;
    }
    
    server {
    	listen 80;
    	server_name example.com;
    
    	root /home/rails-demo/app/current/public;
    	
    	location ~ ^/(assets)/  {
    		gzip_static on; # to serve pre-gzipped version
    		expires max;
    		add_header Cache-Control public;
    	}
    
    	location / {
    		try_files $uri @app;
    	}
    
    	location @app {
    		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    		proxy_set_header X-Forwarded-Proto $scheme;
    		proxy_set_header Host $http_host;
    		proxy_redirect off;
    		proxy_pass http://rails-demo;
    	}
    }
    
    server {
    	listen 80;
    	server_name www.example.com;
    	return 301 http://example.com$request_uri;
    }
  5. Enable the site configuration:

    ln -s /etc/nginx/sites-available/rails-demo /etc/nginx/sites-enabled/rails-demo
  6. Reload nginx if the nginx configuration is ok:

    nginx -t && service nginx reload
  7. Enable port 80 in the firewall:

    ufw allow 80
  8. Check if the application responds as expected, check the log files otherwise:

    tail /var/log/nginx/error.log /home/rails-demo/app/shared/log/*
  9. Change the unicorn configuration to not listen on localhost:4000 anymore:

    nano /home/rails-demo/app/shared/config/unicorn.conf.rb && service rails-demo reload

Re-deploying the app

  1. As root on the server, run visudo:

    EDITOR=nano visudo

    Add a line to configure that the app user is allowed to restart the server:

    rails-demo ALL=(ALL) NOPASSWD: /usr/sbin/service rails-demo restart
  2. Edit config/deploy.rb in the Rails application and enable the service restart command:

    on :launch do
      command "sudo service #{fetch(:user)} restart"
    end
  3. Make a visible change in the app, commit the change and re-deploy:

    nano app/controllers/welcome_controller.rb 
    git commit -a -m "counter changed"
    git push && mina deploy

Apps

website screenshot mac os x
Ralf Ebert Ralf Ebert is an independent software developer and trainer for Mac OS X and iOS. He makes the Page Layers and Straight ahead apps and conducts iOS trainings in Germany since 2009.