How To Run multiple Rails/Rack Apps with MySQL on Nginx and unicorn with Let's Encrypt SSL on Ubuntu 16.04 LTS
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:
- rbenv: Ruby version management
- nginx: Web server
- unicorn: Rack server
- MySQL: Relational database
- Let's Encrypt: Free automated SSL certificates
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.
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.
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.
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.
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.
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:
- Apache2/httpd <$>
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.
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:
- PostgreSQL <$>
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"
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.
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.
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.
If your app needs a connection to the MySQL database you can create it now.
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.
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.
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.