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 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
(...), Arduino, AVR, BaRef, Blosxom, Book, Busy, C++, Charity, Debian, Electronics, Examination, Firefox, Flash, Framework, FreeBSD, Gnome, Hardware, Inter-Actief, IRC, JTAG, LARP, Layout, Linux, Madness, Mail, Math, MS-1013, Mutt, Nerd, Notebook, Optimization, Personal, Plugins, Protocol, QEMU, Random, Rant, Repair, S270, Sailing, Samba, Sanquin, Script, Sleep, Software, SSH, Study, Supermicro, Symbols, Tika, Travel, Trivia, USB, Windows, Work, X201, Xanthe, XBee
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).
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).
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
What do I do if when I start it says permission denied?
Comments are closed for this story.