Last Updated: February 25, 2016
·
3.755K
· toretore

Deploying Rack apps on Ubuntu

This will explain how to properly deploy Rack (including Rails) apps on an Ubuntu server and have it controlled by Upstart. For this demonstration I will use Unicorn, but the procedure is the same for other Ruby app servers.

Best practices for running Ruby processes on a server

  • Be very explicit about everything! Do not assume the presence of an interactive shell or environment variables (this includes $PATH).
  • The previous point means no RVM/rbenv/etc!
  • It also means you have to specify the full path to everything.
  • Be explicit when running a command and specify the parameters.
  • Use a separate Ruby installation for each project that runs on the server.

Install ruby-build

First, let's install ruby-build to help us install the necessary Ruby versions.

$ [sudo] git clone git://github.com/sstephenson/ruby-build.git /opt/ruby-build

Prepare for the installation of Ruby

We're going to keep the various installations of Ruby in /opt/rubies, so let's make that now.

$ [sudo] mkdir /opt/rubies

Before compiling Ruby, it's good to have some optional dependencies installed:

$ [sudo] apt-get install libssl-dev libreadline6-dev zlib1g-dev

Install the required version of Ruby

Now, let's install a specific version of Ruby to a specific directory in /opt/rubies.

/opt/ruby-build/bin/ruby-build 1.9.3-p392 /opt/rubies/1.9.3-p392

This uses ruby-build to download and compile Ruby 1.9.3-p392 and install it in /opt/rubies/1.9.3-p392. You can of course install any version of Ruby you want, into any directory you want. In fact, let's for the sake of demonstration assume we have a Rails app called ShitVille that will run using Ruby 2.0; let's create a completely separate Ruby installation just for this app:

$ [sudo] /opt/ruby-build/bin/ruby-build 2.0.0-p0 /opt/rubies/shitville

Getting that little git to do what I say

There are a number of ways to actually get the code onto the server and update it, but for sake of simplicity I'll just assume that ShitVille lives in a GitHub private repository and we'll just be doing a git pull to update it. To get the code from GitHub you have to use a deploy key, and getting Git to use an SSH key other than the default isn't straight forward.

Now, I assume you have created a separate user to run this app as, let's assume that user is "seaward".

To create a deploy key, run

$ [as seaward] ssh-keygen -t rsa -C "your_email@example.com"

When asked where to store the key, enter ~/.ssh/shitville. This is now the deploy key.

Add this to ~/.ssh/config:

Host gh-shitville
  Hostname github.com
  User git
  IdentityFile ~/.ssh/shitville
  StrictHostKeyChecking no

This will tell SSH to use the deploy key when connecting to the host gh-shitville. Then just git clone the repository:

$ [as seaward] git clone ssh://gh-shitville/gh-username/shitville.git

You can keep the code anywhere you want, but it's nice to have all apps in one place, such as /opt/web:

$ [sudo] mkdir /opt/web
$ [sudo] chown seaward:seaward /opt/web
$ [as seaward] git clone ssh://gh-shitville/gh-username/shitville.git /opt/web/shitville

Bundling

Now that we have Ruby and the app code installed, we need to install the required gems. As ShitVille is a Rails app, these gems are listed in its Gemfile, and we can use Bundler to install them. Now here comes the important part: As we have a separate Ruby version installed for ShitVille, we'll have to be very explicit about what we run. Just bundle isn't going to cut it.

$ cd /opt/web/shitville
$ [sudo] /opt/rubies/shitville/bin/ruby /opt/rubies/shitville/bin/bundle

This will install the necessary gems for the /opt/rubies/shitville Ruby installation.

Test run

To check that ShitVille is ready to be deployed, let's run it from the shell once to see that nothing's missing. I'm assuming that obvious stuff like a database.yml and other necessary configuration is in place.

If Unicorn isn't in ShitVille's Gemfile, just install it:

$ [sudo] /opt/rubies/shitville/bin/ruby /opt/rubies/shitville/bin/gem install unicorn

Then run it:

$ [as seaward] /opt/rubies/shitville/bin/ruby /opt/rubies/shitville/bin/unicorn -Eproduction -p8000 /opt/web/shitville/config.ru

This should run Unicorn in the foreground. It is also the exact command that will be run by Ubuntu's init system Upstart when we've added the init file.

Configure Upstart

Upstart init files live in /etc/init (not init.d!). They're much simpler that traditional init scripts. Here's /etc/init/shitville.conf:

description "ShitVille Rails via Unicorn"

start on runlevel [2345]
stop on runlevel [016]

setuid seaward
setgid seaward

chdir /opt/web/shitville

respawn # Will restart the app if it dies
exec /opt/rubies/shitville/bin/ruby /opt/rubies/shitville/bin/unicorn  -Eproduction -p8000 /opt/web/shitville/config.ru

Start it

$ [sudo] start shitville

$ [sudo] [start|stop|restart] shitville

ShitVille is now managed by Upstart and will start on system boot.

I've only used this strategy with Unicorn, but I imagine it's not much different for e.g. Thin or Puma. Just avoid any magic bullshit that makes assumptions about the environment it's being run in. Do it UNIX style.

One caveat

(Of-fucking-course! But just the one!)

The only caveat I've found with this is that Unicorn's rolling restart functionality can't deal with ruby not being in the PATH. Even though we specify which Ruby binary to use, Unicorn will only run the unicorn script on USR2, which uses the standard !#/usr/bin/env ruby which can't find out Ruby binary. It is probably circumventable, but I haven't looked much into it yet.