Sunday, April 26, 2015

Ruby services in Docker using Passenger

This weekend I was working a little project I wanted to deploy to Digital Ocean. The app was a simple server written Ruby using Sinatra to server some JS. I already had an instance running in Digitual Ocean with Docker installed, so I figured I'd just copy one of my existing docker files and deploy it that way.

Having a look at the Dockerfiles I've used for my existing projects, I realised they all used the standard Ubuntu base and then used a dreaded curl | sudo bash install. I'd recently read this hacker news article about the state of sysadmin in a world with containers, and whilst I disagree with many of the points, I thought it was time to look for a more robust way of dealing with containers.

Enter Phusion and their base-image and passenger dockerfiles. Phusion aims to provide a set of stable, secure and better setup docker images then what most people would be used to. In my case this was definitely true.

The base-image is a standard ubuntu build with a number of tweaks to make it more docker friendly. The main thing I picked up on is they use a version of runit as a lightweight process supervisor to manage the processes running in your container.

The passenger images are aimed at application deployment. There are number of builds for different version of ruby, node etc, and it also comes with Nginx and a few other services bundled together.

Getting down to it, I decided I'd use the ruby21 passenger with everything disabled (ie., Nginx) and just use runit to start my app. This seemed to be the best way to get things up and running as I already had scripts setup to run my app using unicorn. I didn't really want to port the app over to use Nginx and Passenger just yet.

So my Dockerfile ended up looking like this. It's very simple which I like.

My run script was as follows:

And my start script simply called unicorn:

A few things to note:
* Don't put an Entry point in your file.
* Instead use the runit and my_init command that is built in. Basically, you use my_init to start and monitor your daemons rather than using an entry point.

The doco explains how to do this (add a script to /etc/service/my_service_name/run) and it pretty much works out of the box

... except it didn't. On running the service and tailing the logs I got the following:

*** Booting runit daemon...
*** Runit started as PID 9
Rack env: production
Running using: 8080 and rack env: production
Apr 26 05:58:08 67f2f5a43939 syslog-ng[21]: syslog-ng starting up; version='3.5.3'
Rack env: production
Running using: 8080 and rack env: production
master failed to start, check stderr log for details
Rack env: production
Running using: 8080 and rack env: production
master failed to start, check stderr log for details
Rack env: production
Running using: 8080 and rack env: production
master failed to start, check stderr log for details

So the runit process wasn't picking up that service was running, and was constantly trying to restart it. This is obviously not the desired behaviour.

I spent a fair bit of time googling around and reading the doco for the base-image. I also looked at runit in more detail to understand what was going on. It wasn't until I re-read the passenger page that I picked up on this:

Note that the shell script must run the daemon without letting it daemonize/fork it. Usually, daemons provide a command line flag or a config file option for that.

Oh. Right. runit wants to manage the process itself. It doesn't want to be looking for a pid file associated with a daemon that is already running as a daemon. It wants to treat it as a process. The solution is to drop the -D out of the call to unicorn as follows:

And there you go. Now I have a small Dockerfile from a well maintained repository that I can easily use to run any kind of ruby service, be it a worker or a web app.

TL;DR: If you have a ruby process you want to run using passenger--don't daemonize it!


No comments: