Rails Deployment Tutorial

This tutorial shows how to deploy Ruby on Rails applications on Ubuntu Linux 16.04 LTS. 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.0p0 (2016-12-24 revision 57164) [x86_64-darwin16]
    $ gem list rails
    rails (5.0.1)

    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.0
    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.0" > .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.1 x64 as Image (Don’t use the Applications > Ruby and Rails template 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. Add your SSH key so you can log-in to your server without passwords:

    Add SSH Key

    If you don’t have used SSH keys before, create one now:

    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. Disable SSH password authentication:

    nano /etc/ssh/sshd_config && service ssh reload

    Change these:

    PasswordAuthentication no 
    UsePAM no
  4. Check the open ports (should be only SSH):

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

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

    The firewall rules are automatically saved and restored on reboot.

PostgreSQL setup

  1. Install PostgreSQL:

    apt-get install postgresql libpq-dev
  2. Edit the configuration and remove the two lines starting with “host ...” that make PostgreSQL listen on a localhost port - a local socket connection is sufficient for Rails:

    nano /etc/postgresql/9.*/main/pg_hba.conf && service postgresql restart
  3. 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 database using the psql client.

Ruby on Rails setup

  1. Install git, nodejs (the Rails asset pipeline needs a JavaScript interpreter to minify JavaScript files) and 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 git nodejs rng-tools
  2. Install 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.0 && rvm --default use 2.4.0 && 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.0.6

    (this tutorial was tested with Mina version 1.0.6 - if there are newer versions available, use the newer version at your own risk)

  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. Here you can see a diff of the changes compared to the default configuration.

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

    mina setup

    If you’re getting an error when running with Ruby 2.3:

    specification.rb:2158:in `method_missing': undefined method `this'
       for #<Gem::Specification mina-0.3.8> (NoMethodError)

    this is a bug in the rubygems gem that can be fixed by upgrading to the latest version:

    gem update --system
  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 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

    Hint: Using the htop package, you can check for the processes in a nice hierarchical tree view (press F5):

    apt-get install htop

    Processes in htop

  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. Install nginx:

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

    rm /etc/nginx/sites-enabled/default
  3. 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;
    }
  4. Enable the site configuration:

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

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

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

    tail /var/log/nginx/error.log /home/rails-demo/app/shared/log/*
  8. Change the unicorn configuration to not respond 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 and 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.