Graham King

Solvitas perambulum

The Joy of systemd

software systemd

Three years ago when I wrote The Joy of Upstart, that was the easiest way to turn your scripts into daemons. Today the future belongs firmly to systemd, so let’s revisit the Upstart post, and update it for systemd.

systemd is here, it is the default on most major distributions including Ubuntu from 15.04, Fedora from 15, and Red Hat from v7, so you can probably use it now. The main exception is Ubuntu LTS, where we need to wait a few more months for 16.04.

Take this python script: /home/myuser/ez_daemon.py

import time
while 1:
    print("I'm a daemon!")
time.sleep(1)

We’re going to turn it into a daemon in just two lines. Create /etc/systemd/system/ez_daemon.service:

[Service]
ExecStart=/usr/bin/python -u /home/myuser/ez_daemon.py

And that’s it!

  • systemctl start ez_daemon
  • journalctl -f -u ez_daemon. Stdout goes to the journal by default (more below).
  • systemctl status ez_daemon. The status page is actually useful.
  • systemctl stop ez_daemon

A systemd file (such as /etc/systemd/system/ez_daemon.service) is called a unit. Paths in units must be absolute, hence /usr/bin/python. systemd caches units. When you change one you must systemctl daemon-reload to use the latest version. systemd will remind you.

A major difference with Upstart is that stdout and stderr are now buffered. Upstart went the extra mile by wrapping our script with a pseudo-tty to prevent the buffering. systemd uses a pipe, which means by default there is a 4k buffer on stdout and stderr. You won’t see anything in journalctl until that buffer fills. We pass -u to python to prevent this buffering, or we could have called sys.stdout.flush() after each print. Note also that you can’t have end-of-line comments, a comment must be on a line by itself.

Here’s a more complete example:

[Unit]
Description="Example daemon"
# Don't start until the network is available
Requires=network-online.target
After=network-online.target

[Service]
ExecStart=/usr/bin/python -u /home/myuser/bin/ez_daemon.py
Restart=on-failure
User=www-data
Group=www-data
Environment=\
    PYTHONPATH=/srv/example/src/example/ \
    ANSWER=42 \
    "GREET=Hello systemd"
# /usr, /boot and /etc are read-only
ProtectSystem=full    # even safer: ProtectSystem=strict
# $HOME is read only ..
ProtectHome=read-only
# .. except /home/myuser/logs
ReadWriteDirectories=/home/myuser/logs
# /tmp is isolated from all other processes
PrivateTmp=true
# Minimal /dev, no physical device access
PrivateDevices=true
# Don't allow process to raise privileges (e.g. disable suid)
NoNewPrivileges=true
# No network access
PrivateNetwork=true

[Install]
WantedBy=multi-user.target

A particularly exciting part of systemd is the security features enabled by cgroups. The example script includes most of them, which you’ll want to remove as necessary. For example PrivateNetwork wouldn’t be applicable for nginx. Here are full details on systemd security. UPDATE 2018: systemd now has the even safer ProtectSystem=strict and dynamic users.

systemd replaces syslog. As a convenience Ubuntu still runs syslog, messages are available in /var/log/syslog. Fedora no longer runs syslog by default, so /var/log/messages is empty. Instead you use journalctl, which has greatly improving filtering capabilities. Examples in Linux Voice.

Another exciting feature of systemd is socket activation, for example systemd can open port 80 and hand it to your process. You no longer need to start as root, open the port, then drop privileges. This USENIX Login has examples, and is probably the best place to continue reading after this post.

Ubuntu maintains tips for converting Upstart scripts to systemd.

systemd can also replace cron, run containers, and much more. It’s our future, and it’s going to be amazing.