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.