Writing Your Own systemd Service Unit
For too long I kept things alive in a screen session, right up until a reboot took out something someone had handed me as a Java jar. Writing a proper systemd unit takes about ten minutes and gives you start at boot, restart on failure, journal logging and the usual systemctl commands.
Here's a unit for a typical long-running network service, what the directives mean, and why units refuse to start. It assumes CentOS 7, Ubuntu 16.04 or Debian 8+, and an app already installed at /opt/myapp/myapp that runs fine by hand.
A dedicated user
Services should not run as root unless they truly need to. Create a system account without a login shell or home directory:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /opt/myapp
On CentOS the nologin shell lives at /sbin/nologin.
The unit file
Local units belong in /etc/systemd/system/. Never edit files in /lib/systemd/system/ or /usr/lib/systemd/system/; those belong to packages and are overwritten on upgrade.
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My application API server
Documentation=https://wiki.example.com/myapp
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/default/myapp
ExecStart=/opt/myapp/myapp --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
KillMode=mixed
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
The directives worth understanding:
Type=simplemeans systemd considers the service started as soon as the process is forked. UseType=forkingonly for daemons that background themselves, andType=notifyif the program usessd_notify.After=orders startup;Wants=actually pulls the target in. Together they make sure the network is up before the service starts.EnvironmentFile=-with the leading dash means "do not fail if the file is missing." The file holdsKEY=valuelines and is the right place for secrets you do not want in the unit itself.Restart=on-failurerestarts on a non-zero exit or a signal, but not on a clean exit.Restart=alwaysis appropriate if the program is expected to run forever.ProtectSystem=fullmounts/usr,/bootand/etcread-only for the service;ProtectHome=truehides/home. These are available on systemd 214 and later, which includes CentOS 7 (219) and Ubuntu 16.04 (229).
Environment file
sudo nano /etc/default/myapp
MYAPP_PORT=8080
MYAPP_LOG_LEVEL=info
Make it unreadable by others if it contains credentials: sudo chmod 640 /etc/default/myapp.
Load, enable, start
Every time you add or change a unit file, systemd needs to reread it. I forget this one constantly:
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
enable creates a symlink in /etc/systemd/system/multi-user.target.wants/, which is what makes the service start at boot. On systemd 220 and later you can combine both with systemctl enable --now myapp.
Prove it survives
systemctl status myapp
journalctl -u myapp -f
status should show Active: active (running) and the main PID. The journal shows whatever the program writes to stdout and stderr; no log file configuration needed. Confirm it survives a crash by killing it:
sudo kill -9 $(systemctl show -p MainPID --value myapp)
sleep 6
systemctl is-active myapp
After RestartSec seconds it should be active again. Then reboot once and check that it came up on its own. I always do this, because a unit that only works after a manual start is a trap for whoever reboots next.
Why it won't start
Failed to start myapp.service: Unit myapp.service not found.You forgotdaemon-reloadafter creating the file, or the file is not in/etc/systemd/system/.myapp.service: Failed at step EXEC spawning /opt/myapp/myapp: Permission denied. The binary is not executable, or on CentOS it has the wrong SELinux context.chmod +xandrestorecon -v /opt/myapp/myappusually settle it. Note thatExecStartrequires an absolute path;myappalone will not work.Start request repeated too quickly.The program is exiting immediately and hitting systemd's default limit of five starts in ten seconds. Look atjournalctl -u myapp -n 50for the actual error, thensystemctl reset-failed myappbefore trying again.- Service starts but cannot resolve hostnames at boot. You used
After=network.target, which only means the network stack is configured, not that an address is up. Switch tonetwork-online.targetas shown above.
I keep these in version control next to the app. When you want more, systemd.exec(5) and systemd.service(5) are long but worth reading once.