Michael Josephson

Download the slides from the talk I gave at NWRUG in April 2017.

Below are some more detailed notes to accompany the talk too. If you give any of this a try and need more details at all please drop me a line.


The instructions assume a user will be added to the server that will be used for running the hosted applications. In these notes that user is called deploy. The application installation instructions use an app name of demoapp which can be replaced as appropriate. Commands that can be run as the deploy user are prefixed with a $ whereas commands that need to be run as root are prefixed with a #. Instructions are based on using Ubuntu 16.04 LTS

Server set up

  1. Add a separate user that will be used for running the hosted applications:

    # useradd deploy

  2. Set up sudo, this sets things up so the deploy user can run commands via sudo without needing to enter a password, this is required for deployments to run without user intervention:
    1. Add user to sudoers group: # gpasswd -a deploy sudo
    2. Allow sudo commands without entering password: # sudo sh -c 'echo "deploy ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-deploy'
  3. Add a bunch of useful packages so we can build Ruby itself along with any gems that need installing: $ 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 libffi-dev nodejs
  4. Update settings in /etc/ssh/sshd_config:
    1. Disable root logins: PermitRootLogin no
    2. If using SSH keys, set up login via SSH keys only: PasswordAuthentication no

Install Ruby

  1. Set up rbenv and ruby-build for the deploy user:
    1. Switch to home directory: cd
    2. Clone the rbenv git repo: $ git clone git://github.com/sstephenson/rbenv.git .rbenv
    3. Add the rbenv/bin folder to the path: $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
    4. Initialise rbenv when logging in: $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
    5. Install the ruby-build plugin which can be used to build the desired version of Ruby from source: $ git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
    6. Add ruby-build/bin to the path: $ echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bash_profile
    7. Rerun .bash_profile to avoid needing to log out and log back in again to pick up changes above: $ source ~/.bash_profile
    8. Add rbenv-vars plugin to allow a .rbenv-vars file in each application folder to be used to set environment variables for that app: $ git clone https://github.com/rbenv/rbenv-vars.git $(rbenv root)/plugins/rbenv-vars
  2. Install Ruby: $ rbenv install -v 2.4.1
  3. Set the installed version as the default Ruby: $ rbenv global 2.4.1
  4. Disable generation of documentation for gems (not needed in a production environment): $ echo "gem: --no-document" > ~/.gemrc
  5. Install bundler: $ gem install bundler

Install nginx

nginx is used as the public facing web server:

  1. Install nginx: $ sudo apt-get install nginx 

Install postgres

Notes are based on using PostgreSQL but could use MySQL instead:

  1. Install postgres: $ sudo apt-get install postgresql postgresql-contrib libpq-dev

Application prerequisites

One-time changes that should be made to a Rails app before deploying using these notes:

  1. Update the config/puma.rb file to include separate settings for development and production:

Application set up steps

One-time steps needed on the server to set up a new app:

  1. Create a separate postgres user for the app e.g. demoapp
  2. $ sudo -u postgres createuser -s demoapp
  3. Set password for postgres app user: $ sudo -u postgres psql psql (9.5.6, server 9.3.15) Type “help” for help. postgres=# \password demoapp Enter new password: Enter it again: postgres=# \q

  4. Create a bare clone of the app repo (i.e. contains a copy of all of the git history but without a working copy): $ git clone --bare https://github.com/mikej/demoapp.git ~/demoapp_repo
  5. Create the folder to contain the actual running copy of the app: $ mkdir ~/demoapp
  6. Add .rbenv-vars file in ~/demoapp. This file is used by the rbenv-vars plugin to set any necessary environment variables for the app: RAILS_ENV=production SECRET_KEY_BASE= DATABASE_USER=demoapp_ DATABASE_PASSWORD= If your app needs to be able to send emails then this is also the place you'd add any SMTP credentials etc.
  7. Copy/upload the post-receive hook and update-app.sh script into the bare repo folder from step 4 (⚠️ ensure post-receive is readable and executable by deploy user)
  8. Copy/upload a systemd script for your app into /lib/systemd/system and symlink it into /etc/systemd/system/multi-user.target.wants: $ sudo ln -s /lib/systemd/system/demoapp.service /etc/systemd/system/multi-user.target.wants/demoapp.service
  9. Add an nginx config file for your app in /etc/nginx/sites-available and symlink into /etc/nginx/sites-enabled: $ ln -s /etc/nginx/sites-available/demoapp /etc/nginx/sites-enabled/demoapp
  10. Start puma processes and nginx: $ sudo systemctl start demoapp $ sudo systemctl restart nginx

Let’s Encrypt

This section covers generating and installing a Let’s Encrypt certificate once a basic app is up and running. See also: https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04

  1. Install Let’s Encrypt client: $ sudo apt-get install letsencrypt
  2. Generate a certificate: $ sudo letsencrypt certonly -a webroot --webroot-path=/home/mike/demoapp/public -d demoapp.josephson.org

    -a webroot indicates to use webroot authentication: a file is placed in the a folder called .well-known under the web-root path. On running the letsencrypt command, a copy of this file will be requested by Let’s Encrypt to verify ownership of the domain specified.

  3. Generate a strong Diffie-Hellman group: $ sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
  4. Create an nginx config snippet file in /etc/nginx/snippets  that is specific to this certificate e.g. /etc/nginx/snippets/ssl-demoapp.josephson.org.conf:

  5. Create a Configuration Snippet with Strong Encryption Settings. This is a file e.g. /etc/nginx/snippets/ssl-params.conf that can be shared across all your apps, containing a good set of SSL defaults to use for nginx. This is using the recommendations by Remy van Elst on the Cipherli.stsite. You can read more about his decisions regarding the Nginx choices here.

  6. Update the config file in /etc/nginx/sites-available to support SSL and to redirect non-https requests to the corresponding https URL e.g. using config like this.
  7. Restart nginx: $ sudo systemctl restart nginx
  8. Let’s Encrypt certificates are valid for 90 days, but it’s recommended that you renew the certificates every 60 days to allow a margin of error using $ sudo letsencrypt renew. This can be automated by setting up a daily cron task - if there are no certificates due for renewal at the time of running then letsencrypt will indicate this and exit without making any changes.