Run ruby services on OS X with chruby & launchd
January 26, 2017
I’ve been running some ruby services on OS X machines lately and ran into a few roadblocks. Hopefully this will help someone!
Preparing the server
Getting a fresh ruby installed with the excellent chruby and ruby-install is no problem.
You’ll need to have enabled chruby system-wide in order for it to be loaded during deployment.
Otherwise deployment is fairly straightforward. I got along just fine with the sensible defaults provided by capistrano and capistrano-chruby.
Now your fresh code and dependencies are all ready to go. This is where it gets interesting.
Intro to launchd
On a Linux OS you have a number of options: runit, upstart, systemd etc. But to manage services on OS X you really want to be using launchd.
At first it seem a bit counterintuitive, but there are some tools and abstractions that make it a little easier to manage.
- LaunchControl provides a GUI for managing and creating launchd services. It’s especially useful when creating a new service from scratch.
- lunchy is a wrapper for the not-intuitive launchctl CLI provided by OS X.
A lot of launchd itself is outside of the scope of this post. For a good reference check out the launchd Tutorial.
Running your ruby code
The key to running a chruby-installed ruby in a launchd job definition is the chruby-exec
command.
For example, on my system I have ruby-2.4.0
installed and I want to run a rake task called roll_call
. I could run the following command.
/usr/local/bin/chruby-exec ruby-2.4.0 -- bundle exec rake roll_call
Which translates to the following in a job definition:
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/chruby-exec</string>
<string>ruby-2.4.0</string>
<string>--</string>
<string>bundle</string>
<string>exec</string>
<string>rake</string>
<string>roll_call</string>
</array>
However, running bundle exec
won’t work from outside of a ruby project directory. So set the working directory to the currently deployed project.
<key>WorkingDirectory</key>
<string>/Users/giles/roll-call/current</string>
This job will now successfully run but in the development environment. Easily fixed by setting an environment variable.
<key>EnvironmentVariables</key>
<dict>
<key>RACK_ENV</key>
<string>production</string>
</dict>