Glider
"In het verleden behaalde resultaten bieden geen garanties voor de toekomst"
About this blog

These are the ramblings of Matthijs Kooijman, concerning the software he hacks on, hobbies he has and occasionally his personal life.

Most content on this site is licensed under the WTFPL, version 2 (details).

Questions? Praise? Blame? Feel free to contact me.

My old blog (pre-2006) is also still available.

See also my Mastodon page.

Sun Mon Tue Wed Thu Fri Sat
           
9
         
Powered by Blosxom &Perl onion
(With plugins: config, extensionless, hide, tagging, Markdown, macros, breadcrumbs, calendar, directorybrowse, feedback, flavourdir, include, interpolate_fancy, listplugins, menu, pagetype, preview, seemore, storynum, storytitle, writeback_recent, moreentries)
Valid XHTML 1.0 Strict & CSS
Running Ruby on Rails using Systemd socket activation

Ruby on Rails logo

On a small embedded system, I wanted to run a simple Rails application and have it automatically start up at system boot. The system is running systemd, so a systemd service file seemed appropriate to start the rails service.

Normally, when you run the ruby-on-rails standalone server, it binds on port 3000. Binding on port 80 normally requires root (or a special capability enabled for all of ruby), but I don't want to run the rails server as root. AFAIU, normal deployments using something like Nginx to open port 80 and let it forward requests to the rails server, but I wanted a minimal setup, with just the rails server.

An elegant way to binding port 80 without running as root is to use systemd's socket activation feature. Using socket activation, systemd (running as root) opens up a network port before starting the daemon. It then starts the daemon, which inherits the open network socket file descriptor, with some environment variables to indicate this. Apart from allowing privileged ports without root, this has other advantages such as on-demand starting, easier parallel startup and seamless restarts and upgrades (none of which is really important for my usecase, but it is still nice :-p).


Making it work

To make this work, the daemon (rails server in this case) needs some simple changes to use the open socket instead of creating a new one. I could not find any documentation or other evidence that Rails supported this, so I dug around a bit. I found that Rails uses Rack, which again uses Thin, Puma or WEBrick to actually set up the HTTP server. A quick survey of the code suggests that Thin and WEBrick have no systemd socket support, but Puma does.

I did find a note saying that the rack module of Puma does not support socket activation, only the standalone version. A bit more digging in my Puma version supported this, but it seems that some refactoring in the 3.0.0 release (commit) should allow Rack/Rails to also use this feature. Some later commits add more fixes, so it's probably best to just use the latest version. I tested this succesfully using Puma 3.9.1.

One additional caveat I found is that you should be calling the bin/rails command inside your rails app directory, not the one installed into /usr/local/bin/ or wherever. It seems that the latter calls the former, but somewhere in that process closes all open file descriptors, losing the network connection (which then gets replaces by some other file descriptor, leading to the "for_fd: not a socket file descriptor" error message).

Setting this up

After setting up your rails environment normally, make sure you have the puma gem installed and add the following systemd config files, based on the puma examples. First, /etc/systemd/system/myrailsapp.socket to let systemd open the socket:

[Unit]
Description=Rails HTTP Server Accept Sockets

[Socket]
ListenStream=0.0.0.0:80

# Socket options matching Puma defaults
NoDelay=true
ReusePort=true
Backlog=1024

Restart=always

[Install]
WantedBy=sockets.target

Then, /etc/systemd/system/myrailsapp.service to start the service:

[Service]
ExecStart=/home/myuser/myrailsapp/bin/rails server puma --port 80 --environment production
User=myuser
Restart=always

[Install]
WantedBy=multi-user.target

Note that both files should share the same name to let systemd pass the socket to the service automatically. Also note that the port is configured twice, due to a limitation in Puma. This is just a minimal service file to get the socket activation going, there are probably more options that might be useful. This blogpost names a few.

After creating these files, enable and start them and everything should be running after that:

$ sudo systemctl enable myrailsapp.socket myrailsapp.service
$ sudo systemctl start myrailsapp.socket myrailsapp.service
 
1 comment -:- permalink -:- 10:42
Copyright by Matthijs Kooijman - most content WTFPL