Systemd has, over the strident objections of many strident people, become the default init system for a surprising number of linux distributions. Though I’ve been aware of the drama, the eye-rolling, the uh.. enterprising nature of systemd, I really have only just started using it myself. All the wailing and gnashing of teeth surrounding it left me unsure what to expect.
Recently I needed to get a Proof-Of-Concept app I built running so a client could use it on their internal network to evaluate it. Getting my Rails app to start on boot was pretty straight forward and I’m going to be using this again so I thought I would document it here.
First I created a “rails” user and group, and in /home/rails
I installed my usual Rbenv setup. The fact that only root is allowed to listen to ports below 1024, conflicts with my plan to run my app with the “rails” user and listen on port 80. The solution is setcap:
setcap 'cap_net_bind_service=+ep' .rbenv/versions/2.2.2/bin/bundle
With that capability added, I set up my systemd unit file in /usr/lib/systemd/system/myapp.service
and added the following:
[Unit] Description=MyApp Requires=network.target Requires=arangodb.service [Service] Type=simple User=rails Group=rails WorkingDirectory=/home/rails/myapp ExecStart=/usr/bin/bash -lc 'bundle exec rails server -e production --bind 0.0.0.0 --port 80' TimeoutSec=30 RestartSec=15s Restart=always [Install] WantedBy=multi-user.target
The secret sauce that makes this work with rbenv is the “bash -l
” in the ExecStart
section. This means that the bash will execute as though it was a login shell, meaning that the .bashrc file with all the PATH exports and rbenv init stuff will be sourced before the command I give it will be run. In other words, exactly what happens normally.
From there, I just start the service like all the rest of them:
systemctl enable myapp.service systemctl start myapp.service
This Just Works™ and got the job done, but in the process I find I am really starting to appreciate Systemd. Running daemons is complicated, and with a the dropping of privileges, ordering, isolation and security options, there is a lot to get right… or wrong.
What I am liking about Systemd is that it is taking the same functions that Docker is built on, namely cgroups and namespacing, and giving you a declarative way of using them while starting your process. Doing so puts some really nice (and otherwise complicated) security features within reach of anyone willing to read a man
page.
PrivateTmp=yes
is a great example of this. By simply adding that to the unit file above (which you should if you call Tempfile.new
in your app) closes off a bunch of security problems because systemd “sets up a new file system namespace for the executed processes and mounts private /tmp and /var/tmp directories inside it that is not shared by processes outside of the namespace”.
Could I get the same effect as PrivateTmp=yes
with unshare
? With some fiddling, but Systemd makes it a zero cost option.
There is also ProtectSystem=full
to mount /boot
, /usr
and /etc
as read only which “ensures that any modification of the vendor supplied operating system (and optionally its configuration) is prohibited for the service”. Systemd can even handle running setcap
for me, resulting in beautiful stuff like this, and there is a lot more in man systemd.exec
besides.
For me I think one of the things that has become clear over the last few years is that removing “footguns” from our software is really important. All the work that is going into the tools (like rm -rf) and languages (Rust!) we use less spectacularly dangerous is critical to raising the security bar across the industry.
The more I learn about Systemd the more it seems to be a much needed part of that.
Trying to do the same on my raspberry pi – getting “Excess arguments.” error.
The problem of “Excess arguments.” is because of a typo:
‘systemd enable myapp.service’ should be ‘systemctl enable myapp.service’
‘systemd start myapp.service’ should be ‘systemctl start myapp.service’
Thanks for pointing that out. Corrected!
You have just saved my life with this little trick!
`ExecStart=/usr/bin/bash -lc ‘…’`
Note that instead of using a capability to allow port 80, you can also use systemd socket activation to let systemd open up the port and pass it onto rails. This requires some specific setup, which I detailed on my blog: http://www.stderr.nl/Blog/Software/Rails/RailsSocketActivation.html