February 05, 2020

Rails Deployment Tutorial

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

The deployment process will be fully automated so that it requires only one shell command to deploy a new version:

Rails deploy to server

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

Folder structure for deploying rails apps

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 only trusted users have shell access to the server.

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.

Setting up an example app

Fixing server issues and app-specific issues at the same time can be very error-prone. When you work through this tutorial the first time, I recommend to get a simple demo application running, then deploy your own Rails app. You can use the following steps to create a simple example app:

  1. Check your Ruby and Rails versions:

    $ ruby --version
    ruby 2.6.5p114 (2019-10-01 revision 67812)
    $ gem list rails
    rails (6.0.2.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.6.5
    rvm use 2.6.5
    gem update --system && gem install bundler rails
    
  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.6.5" > .ruby-version
    
  4. Edit the Gemfile and add a production group with the gems pg (PostgreSQL) and unicorn (the Rack HTTP server). Wrap the sqlite3 gem in a group for development and test so that it doesn't get installed in production. Add the mina gem for the development environment:

    group :production do
      gem 'pg'
      gem 'unicorn'
    end
    
    group :development, :test do
      gem 'sqlite3'
      gem 'mina', '1.2.3'
    end
    

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

    This tutorial was tested with Mina version 1.2.3 - if there are newer versions available, it's recommended to use 1.2.3 first and then update to the new version.

  5. Install all dependencies locally skipping those from the production group:

    bundle install --without=production
    
  6. Generate a controller for serving a 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:

    Rails Beispiel-App
  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 Github and push the project to the repository, for example:

    git remote add origin git@github.com:example/rails-demo.git
    git push -u origin master
    

VM setup

  1. Sign-up at DigitalOcean (if you use this link, you'll get $100 in credit so you can set up a VM for free).

  2. Create a new droplet and select Ubuntu 18.04.3 (LTS) x64 or Debian 10.2 x64 as Image. Also select the size for your droplet (warning: you can easily upgrade to larger sizes, but it is not possible to downgrade to a size with a smaller disk):

    Create a DigitalOcean droplet
  3. Add 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.

  4. Create the Droplet and wait for the VM to be created, then log-in to the system:

    ssh root@serverip
    
  5. Set the timezone:

    dpkg-reconfigure tzdata
    
  6. Check the open ports (should be only SSH):

    netstat --listening --tcp
    
  7. Update all packages:

    apt update && apt upgrade
    
  8. Enable the firewall so that unconfigured services will not be exposed (the IP ranges of VM providers are frequently scanned for not fully configured servers):

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

    The firewall rules are automatically saved and restored on reboot.

  9. Install the following required packages:

    apt install curl git nginx postgresql libpq-dev
    
  10. For the Rails asset pipeline which uses webpack, you need to install Node.js and yarn which are provided as 3rd party repositories. Install both packages according to the following instructions:

    • Node.js (see Installation instructions for Node.js v12.x)
    • yarn
  11. Install Ruby Version Manager (RVM) according to the instructions on their website (use the gpg command instead of gpg2 for getting the certificate).

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

  13. Install Ruby and the bundler gem:

    rvm install 2.6.5 && rvm --default use 2.6.5 && gem update --system && gem install bundler
    

Setting up the Ruby on Rails app

  1. Create a user and a database for the application:

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

    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.

  2. Create a user for the app:

    adduser $APP_NAME --disabled-password
    
  3. Add your SSH public key to the user home so you can log-in as the app user.

    To copy the SSH keys from root to the user:

    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
    
  4. Log-out and log-in as the app user:

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

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

    Add the deployment key to the repository
  7. Locally, add a config/deploy.rb configuration file to the Rails project (example configuration):

    cd demo/
    curl https://raw.githubusercontent.com/ralfebert/rails-deploy/master/example-deploy.rb > config/deploy.rb
    

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

    The example configuration for Mina has been customized for this tutorial to set up rvm according to .ruby-version and to automatically create a database.yml and secrets.yml configuration file on the server during mina setup (show diff).

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

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

    nano app/shared/config/database.yml
    nano app/shared/config/secrets.yml
    
  10. Deploy the app to the server:

    mina deploy
    

    This will 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
    
  11. To test the app without exposing it to the public, log-in to the server forwarding a port:

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

    curl https://raw.githubusercontent.com/ralfebert/rails-deploy/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')
    
  13. To test the app installation, run the unicorn Rails server directly:

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

    Otherwise, check the application log files:

    tail ~/app/shared/log/*
    

Unicorn as systemd service

  1. Log-in as root on the server side and create a systemd service for unicorn:

    curl https://raw.githubusercontent.com/ralfebert/rails-deploy/master/example-systemd > /etc/systemd/system/rails-demo.service
    

    Example configuration:

    [Unit]
    Description=rails-demo service
    After=network.target
    
    [Service]
    Type=forking
    WorkingDirectory=/home/rails-demo/app/
    User=rails-demo
    ExecStart=/usr/local/rvm/bin/rvm in /home/rails-demo/app/current/ do bundle exec unicorn -D -c /home/rails-demo/app/shared/config/unicorn.conf.rb -E production
    SyslogIdentifier=unicorn-rails-demo
    # stop by sending only the main process a SIGQUIT signal
    KillMode=process
    KillSignal=SIGQUIT
    # Enable reloading unicorn via HUP signal
    ExecReload=/bin/kill -HUP $MAINPID
    # Try to restart the service after 1 second
    Restart=always
    RestartSec=1
    # Path to Unicorn PID file (as specified in unicorn configuration file)
    PIDFile=/home/rails-demo/app/shared/unicorn.pid
    
    [Install]
    WantedBy=multi-user.target
    
  2. Customize the service configuration: Replace all occurences of rails-demo to the name of your actual app/user (the systemd configuration doesn't support variables so you have to manually replace the name in the service configuration):

    nano /etc/systemd/system/rails-demo.service
    
  3. Test starting and stopping the server:

    systemctl daemon-reload
    systemctl start rails-demo
    systemctl status rails-demo
    systemctl restart rails-demo
    
  4. Check the Rails application log files and test the app using SSH port forwarding:

    tail /home/rails-demo/app/shared/log/*
    
  5. Enable the service on boot:

    systemctl enable rails-demo
    

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. Disable the default page:

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

    curl https://raw.githubusercontent.com/ralfebert/rails-deploy/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; # serve pre-gzipped version
    		expires 1M;
    		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 && systemctl reload nginx
    
  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 listen on localhost:4000 anymore:

    nano /home/rails-demo/app/shared/config/unicorn.conf.rb
    systemctl restart rails-demo
    

Re-deploying the app

  1. Allow the app user to restart the server app - as root on the server, run visudo:

    EDITOR=nano visudo
    

    add a line at the end to configure that the app user is allowed to restart the server:

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

    on :launch do
      command "sudo systemctl restart #{fetch(:user)}"
    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