Skip to content

Instantly share code, notes, and snippets.

@kmcphillips
Last active May 26, 2020 12:43
Show Gist options
  • Save kmcphillips/8c4b6b5ddc89ed2a83c30ee7e327f24f to your computer and use it in GitHub Desktop.
Save kmcphillips/8c4b6b5ddc89ed2a83c30ee7e327f24f to your computer and use it in GitHub Desktop.
How To Run multiple Rails/Rack Apps with MySQL on Nginx and Unicorn with Let's Encrypt SSL on Ubuntu 16.04 LTS

How To Run multiple Rails/Rack Apps with MySQL on Nginx and unicorn with Let's Encrypt SSL on Ubuntu 16.04 LTS

Introduction

The goal of this tutorial is to fully configure a fresh Ubuntu 16.04 drop to run multiple Rack compliant (Rails/Sinatra) Ruby app applications. Each of the long list of tools used has its own detailed documentation, but this guide focuses on how to link them all together into a production-ready server.

There are in almost every case several tools that will fulfill a similar purpose, but in this guide we will use:

Some of the steps are optional, and are marked as such, depending on the requirements of your app.

We will focus on running multiple apps on a single server with different SSL certs and vhosts for each domain. Many pre-packaged images or guides configure apps globally, but this guide sets up the droplet you can scale it up to run as many apps as you'd like.

Prerequisites

Before you begin this guide you'll need the following:

  • A fresh DigitalOcean droplet running Ubuntu 16.04 LTS
  • SSH access as root to the droplet, either with a passowrd or an SSH key.
  • A domain with its nameservers set to DigitalOcean.
  • Optionally a Rack compliant Ruby app to deploy. If not, a super simple example app is included.

Step 1 — Configuring the deploy user

It's good practice to not run apps as root, so we'll start by creating a regular user named deploy.

adduser deploy

Give your user a password, then select the default value for the rest of the prompts.

Next, add the same user to the admin group so it has access to sudo:

adduser deploy admin

Log out now and SSH back into your server as the deploy user. We will execute everything from now on as this user, and use sudo as necessary.

Step 2 — Installing rbenv

We are going to use rbenv to manage our Ruby versions. A Ruby version manager installs multiple versions of Ruby without conflict, allows applications to select their Ruby version, and prevents gems being installed globally with root permissions.

<$>[note] Note: Alternatives to rbenv:

First, we will install the packages that rbenv depends on:

sudo apt-get install build-essential libcurl4-openssl-dev libffi-dev libreadline-dev libssl-dev libxml2-dev libxslt1-dev zlib1g-dev

We are going to check out rbenv from source and install it in the home directory for the deploy user. We can simply fetch this with git:

git clone https://github.com/rbenv/rbenv.git ~/.rbenv

Switch to that directory and build from source:

cd ~/.rbenv
src/configure
make -C src

Then as root create an entry to load rbenv under profile.d:

[label /etc/profile.d/rbenv.sh]
if [ -d "$HOME/.rbenv" ]; then
  export PATH=$HOME/.rbenv/bin:$PATH;
  export RBENV_ROOT=$HOME/.rbenv;
  eval "$(rbenv init -)";
fi

This allows you to swtich Ruby versions, but none are installed yet. The ruby-build plugin will let us easily install versions of Ruby:

git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Log out and log back in to reload your shell config.

We can validate that rbenv is installed with:

which rbenv

Next we will use the rbenv to install a version of Ruby.

Step 3 — Installing Ruby 2.3.1

Now that rbenv manages our Ruby versions for us, installing a version is a snap. We can install 2.3.1 with simply:

rbenv install 2.3.1

And set it as the default global Ruby version with:

rbenv global 2.3.1

Finally, install bundler to manage gems:

gem install bundler

We can validate that Ruby is installed with:

ruby --version

The install command can be called for each version of Ruby requied.

Step 4 — Installing nginx

We are going to use nginx as our web server. This server will manage connections to their correct domains, serve static files, and proxy requests to our app.

<$>[note] Note: Alternatives to nginx:

First, install the nginx package:

sudo apt-get install nginx

By default nginx on Ubuntu runs as the www-data user, and that's fine.

Installing the package also starts nginx as a service on our system. It will load any files in /etc/nginx/sites-enabled and server those configurations. It comes with a default file that serves a welcome page. We can edit this file, and completely replace it with the following:

[label /etc/nginx/sites-enabled/default]
server {
        listen 80 default_server;
        listen [::]:80 default_server;
        return 404;
}

This tells nginx to serve a 404 not found by default for any request it does not match to any other configuration.

You can restart nginx for this change to take effect:

sudo service nginx restart

We can validate that nginx is running by running:

wget localhost

Though confusing, a positive result is a ERROR 404: Not Found meaning that we are correctly serving our not found page.

Step 5 — Installing MySQL (Optional)

A database step isn't actually necessary so feel free to skip this step. But for completeness we can install MySQL for any Ruby apps that may need it.

<$>[note] Note: Alternatives to MySQL:

First, install the MySQL client and server packages:

sudo apt-get install mysql-common mysql-server mysql-client

During this process you will be prompted to set the password for your root user. Keep this safe, and only use it for creating new users and databases. Fortunately MySQL has very sane default settings for security and is only listening for connections from localhost.

We can validate that MySQL is running and configured with:

mysql -uroot -p -e "SELECT 1"

Step 6 — Configuring the app home directory

We can run our apps from any directory that makes sense, so we will pick and create /var/apps. Each directory inside will be an app that responds to the Rack Ruby webserver interface, such as Ruby on Rails apps or Sinatra apps.

Create the directory:

sudo mkdir /var/apps

And transfer ownership to the app user:

sudo chown deploy /var/apps

Now that our server has all the required software installed and configured, the next steps setup and configure apps and domains to run on it. They can be run for each app and domain.

Step 7 — Configuring DigitalOcean DNS

From the Networking tab on your DigitalOcean dashboard add your new domain <^>example.com<^>. You should see the following preconfigured:

example.com ns1.digitalocean.com.
example.com ns2.digitalocean.com.
example.com ns3.digitalocean.com.

Now add the following records, where <^>111.111.111.111<^> is the IP address of your droplet:

  • A @ <^>111.111.111.111<^>: This directs from this domain to your server.
  • CNAME www @: This aliases adds a www alias to the root domain.

You can also add MX records if you wish to direct mail from this domain to this server, or another service.

Step 8 — Setup your Rack app

We are going to structure the directory of our app to use the conventions that the Capistrano deployment tool expects. We won't be configuring deployment in this tutorial, but no further changes to the server are needed for a deploy script to push code to it.

Create the directory for the app:

mkdir /var/apps/<^>example.com<^>

The app home directory contains current directory where our app will run and live:

mkdir /var/apps/<^>example.com<^>/current

And some shared directories for logs, process files, and sockets:

mkdir /var/apps/<^>example.com<^>/shared
mkdir /var/apps/<^>example.com<^>/shared/tmp
mkdir /var/apps/<^>example.com<^>/shared/tmp/pids
mkdir /var/apps/<^>example.com<^>/shared/tmp/sockets
mkdir /var/apps/<^>example.com<^>/shared/log

You can place your Rack app into this directory and move on to the next step. But for the purpose of completeness we can create a super simple Sinatra app to test with. From inside the current directory create the Gemfile:

[label /var/apps/<^>example.com<^>/current/Gemfile]
source "https://rubygems.org"
gem "sinatra"
gem "unicorn"

And our application:

[label /var/apps/<^>example.com<^>/current/config.ru]
require "sinatra"

class App < Sinatra::Base do
  get "/" do
    "Hello, world!"
  end
end

run App

Then install your gems with Bundler:

bundle install

Next, we will configure our application server to run and manage our worker processes for our app.

Step 9 — Setting up MySQL database (Optional)

If your app needs a connection to the MySQL database you can create it now.

Step 10 — Installing unicorn

We are going to use unicorn as our application server. Unicorn is a robust multi-process web server suitable for production use, intended to be the minimum layer between Rack apps and nginx web servers.

<$>[note] Note: Alternatives to unicorn:

The correct version of unicorn is installed as a Gemfile dependency of the rack app configured in the previous step. So there is no actual application code to install. But we need to create the configuration file to run unicorn with:

mkdir /var/apps/<^>example.com<^>/current/config
[label /var/apps/<^>example.com<^>/current/config/unicorn.rb]
project_dir = "/var/apps/<^>example.com<^>"
current_dir = "#{ project_dir }/current"
shared_dir = "#{ project_dir }/shared"

before_exec do |server|
  ENV["BUNDLE_GEMFILE"] = "#{ current_dir }/Gemfile"
end

working_directory current_dir
worker_processes 2
preload_app true
timeout 30
listen "#{ shared_dir }/tmp/sockets/unicorn.sock", backlog: 64
stderr_path "#{ shared_dir }/log/unicorn.stderr.log"
stdout_path "#{ shared_dir }/log/unicorn.stdout.log"
pid "#{ shared_dir }/tmp/pids/unicorn.pid"

We want to make sure our unicorn processes for each app are managed independently, run on server start, and can be stopped and started conveniently. To do this, we will use an init.d script for each. As root or with sudo, create this file:

[label /etc/init.d/unicorn-<^>example.com<^>]
#!/bin/sh

### BEGIN INIT INFO
# Provides:          unicorn-<^>example.com<^>
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn for <^>example.com<^>
# Description:       starts unicorn for <^>example.com<^> using start-stop-daemon
### END INIT INFO

set -e

USAGE="Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"

USER="deploy"
APP_NAME="<^>example.com<^>"
APP_ROOT="/var/apps/$APP_NAME"
ENV="production"
PATH="/home/$USER/.rbenv/shims:/home/$USER/.rbenv/bin:$PATH"
CMD="cd $APP_ROOT/current && bundle exec unicorn -c $APP_ROOT/current/config/unicorn.rb -E $ENV -D"
PID="$APP_ROOT/shared/tmp/pids/unicorn.pid"
OLD_PID="$PID.oldbin"

cd $APP_ROOT || exit 1

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
  test -s $OLD_PID && kill -$1 `cat $OLD_PID`
}

case $1 in
  start)
    sig 0 && echo >&2 "Already running" && exit 0
    echo "Starting $APP_NAME"
    su - $USER -c "$CMD"
    ;;
  stop)
    echo "Stopping $APP_NAME"
    sig QUIT && exit 0
    echo >&2 "Not running"
    ;;
  force-stop)
    echo "Force stopping $APP_NAME"
    sig TERM && exit 0
    echo >&2 "Not running"
    ;;
  restart|reload|upgrade)
    sig USR2 && sleep 5 && echo "reloaded $APP_NAME" && oldsig QUIT && echo "Killing old master" `cat $OLD_PID` && exit 0
    echo >&2 "Couldn't reload, starting '$CMD' instead"
    su - $USER -c "$CMD"
    ;;
  rotate)
    sig USR1 && echo rotated logs OK && exit 0
    echo >&2 "Couldn't rotate logs" && exit 1
    ;;
  *)
    echo >&2 $USAGE
    exit 1
    ;;
esac

There's quite a lot going on here, but it is mostly a standard init script that talks to unicorn and is run with a standard start/restart/stop interface. It uses the USR2 and QUIT signals to rotate processes, and points to the unicorn.pid files in the project directory to track the process numbers.

Make the script executable:

sudo chmod +x /etc/init.d/unicorn-<^>example.com<^>

And start unicorn:

sudo /etc/init.d/unicorn-<^>example.com<^> start

We can validate that rbenv is installed with:

ps ax | grep unicorn

Now that our unicorn app is running, it will server requests to local requests only. To handle web requests we will use nginx and proxy those into unicorn.

Step 11 — Configuring nginx vhost

A vhost file tells the web server to listen for requests on our domain, serve static files, and proxy app requests to unicorn. As root, create the file:

[label /etc/nginx/sites-available/<^>example.com<^>.conf]
upstream <^>example_com<^> {
    server unix:/var/apps/<^>example.com<^>/shared/tmp/sockets/unicorn.sock fail_timeout=0;
}

server {
    listen 80;
    listen [::]:80;
    server_name *.<^>example.com<^> <^>example.com<^>;

    root /var/apps/<^>example.com<^>/current/public;

    try_files $uri/index.html $uri @<^>example_com<^>;

    location @<^>example_com<^> {
        proxy_pass http://<^>example_com<^>;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    }

    client_max_body_size 4G;
    keepalive_timeout 10;
}

This tells nginx to listen on port 80 on our domain, serve static files from the current/public directory, and proxy the rest to our unicorn socket.

Activate this vhost by linking it:

sudo ln -s /etc/nginx/sites-available/<^>example.com<^>.conf /etc/nginx/sites-enabled/<^>example.com<^>.conf

And then restarting nginx:

sudo service nginx restart

The server is now configured and running and serving your application. Validate by visiting your domain in your browser.

As a last step, we can optionally encrypt our traffic with an SSL cert.

Step 12 — Using Let's Encrypt for SSL (Optional)

Conclusion

Your server is now production ready and serves traffic for your app.

To add another app on another domain to the same server, simply run steps 7 to 12 again.

A good next step would be to add Capistrano to your Rails or Rack app to automate deployment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment