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
- 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.
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/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 "firstname.lastname@example.org"
When asked where to store the key, enter
~/.ssh/shitville. This is now the deploy key.
Add this to
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
$ [sudo] mkdir /opt/web $ [sudo] chown seaward:seaward /opt/web $ [as seaward] git clone ssh://gh-shitville/gh-username/shitville.git /opt/web/shitville
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.
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.
Upstart init files live in
init.d!). They're much simpler that traditional init scripts. Here's
description "ShitVille Rails via Unicorn" start on runlevel  stop on runlevel  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
$ [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.
(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.