Ruby apps under runit: notes to self
Today I learned (2019-06-25)
Tagged:
I was recently migrating nearly ten-year-old Ruby software from an Ubuntu 10:04 VM to something more recent. It took me way longer than I expected, due to two major snags.
Snag one: Bundler and per-user runsvdir
The apps in their original Lucid Lynx setting were run from a bash script that resembled this:
while (/bin/true); do
(bundle exec 'thin -C config_foo.yml -R config.ru start' 2>&1)
echo "Server died at `date`. Respawning.." >&2
sleep 2
done
The app stayed up for years, so I guess this unsophisticated approach was good enough. For ease of deployment (and improved logging, see point #2 below), I decided to move all ruby web apps on this VPS to a user-local runit supervision tree.
In practice, this means that there's a /etc/service/webuser-sv
directory,
containing a runit service which launches a nested runsvdir
program as the
webuser-sv
user.
$ cat /etc/service/webuser-sv
#!/bin/sh
exec 2>&1
exec chpst -uwebuser runsvdir /home/webuser/service
Now, I can define all of webuser
's ruby apps as entries under
/home/webuser/service/*
, and have them supervised without bash hackery.
The snag was that the apps would crash with this error, but only when run as part of the runit supervision tree:
2019-06-25_11:27:37.51241 `/` is not writable.
2019-06-25_11:27:37.51244 Bundler will use `/tmp/bundler/home/unknown' as your home directory temporarily.
But if I ran the runit run
scripts by hand, the apps started up and worked correctly.
After a lot of false starts and pointless github issue rabbit-holes, I realized
that the runsvdir
process managing the user-local supervision tree was
launched with chpst by the master
runit supervisor. Specifying a UID for chpst
with -u
does not automatically
mean that the profile for this user gets loaded. In particular, not even
$HOME
was configured in the runit supervisor environment.
Bundler needs '$HOME' to be set, otherwise it gets confused.
Hence, my runit run files now look like this:
exec 2>&1
export RUBYOPT=-W0
export HOME=/home/web
export LANG=en_US.UTF-8
cd /home/web/webapp.1.com
exec bundle exec rackup -E production -s thin -o 0.0.0.0 -p 4567 config.ru
Duplication could be further removed by setting up an envdir
, and running the
user-level runsvdir
with this envdir passed to chpst
... but the above
solution is good enough for today.
Snag two: Don't ever redirect $stdin and $stderr
Runit has a wonderfully clean approach to logging, predating the 12-factor app, but very similar in spirit. Run your app in the foreground, and pipe its output to your log processor. This way applications themselves never need to concern themselves with logging frameworks and infrastructure.
Now ten years ago, when I launched these apps from bash scripts, they apps themselves definitely needed to know what to do with their logs. Hence, these two monstrous lines:
settings.configure do |s|
$stdout = File.open("log/log_out.txt","a")
$stderr = File.open("log/log_err.txt","a")
These two lines had me stumped as to why an app was silently crashing at startup with nothing in the runit logs. After removing the two lines I was able to see the real reason for the crash (some missing assets).
Sure, the exception had been logged all along to log/log_err.txt
, but I'd
completely forgotten to look there, expecting the logging to be handled by
runit's log facility.
Never redirect $stdout and $stdin inside the app, kids. Your future self will thank you.