systemd service unit basics

2025-08-05 — systemd, services

I keep needing to look up the difference between Wants= and Requires=, or which Type= to use for a given daemon. Writing it down.

Unit file locations

  • /usr/lib/systemd/system/ — package-provided units (don't edit)
  • /etc/systemd/system/ — local overrides and custom units
  • /etc/systemd/system/foo.service.d/override.conf — drop-in overrides for an existing unit (preferred over editing package files)

To create a drop-in without editing by hand: systemctl edit foo.service

Unit file structure

A service unit has three sections. Not all are required.

[Unit]

[Unit]
Description=My background service
Documentation=https://example.com/docs
After=network-online.target
Wants=network-online.target

After= — ordering only. This unit starts after the listed ones, but doesn't require them to succeed.

Wants= — soft dependency. systemd will try to start the listed units, but if they fail, this unit still starts.

Requires= — hard dependency. If the required unit fails or stops, this unit is also stopped. Use sparingly.

[Service]

[Service]
Type=simple
User=myuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.yaml
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal

Type= — how systemd tracks readiness:

  • simple — process starts, systemd considers it ready immediately. Default for most things.
  • exec — like simple but waits until the binary has actually been exec'd. Safer for scripted wrappers.
  • forking — old-style daemon that forks and exits the parent. Needs PIDFile=.
  • oneshot — runs, exits, done. systemd waits for it to finish. Good for setup scripts and RemainAfterExit=yes.
  • notify — daemon signals readiness via sd_notify(). Requires the app to support it.

Restart= — when to restart automatically:

  • no — never (default)
  • on-failure — on non-zero exit code or signal
  • always — always, even on clean exit
  • on-abnormal — on signals and watchdog timeout, not clean exit

[Install]

[Install]
WantedBy=multi-user.target

WantedBy=multi-user.target is the right setting for most server services — it means "start this when the system reaches multi-user mode." This is what systemctl enable uses to create the symlink.

Essential systemctl commands

CommandWhat it does
systemctl start fooStart now
systemctl stop fooStop now
systemctl restart fooStop then start
systemctl reload fooReload config without full restart (if supported)
systemctl enable fooStart on boot
systemctl disable fooDon't start on boot
systemctl status fooCurrent state + recent log lines
systemctl is-active fooExits 0 if running (useful in scripts)
systemctl is-enabled fooExits 0 if enabled for boot
systemctl cat fooPrint the unit file as loaded (including overrides)
systemctl list-units --type=serviceAll active service units
systemctl list-units --failedUnits in failed state
systemctl daemon-reloadRe-read unit files after changes

Reading logs with journalctl

# All logs for a service
journalctl -u myservice

# Follow live (like tail -f)
journalctl -fu myservice

# Last 50 lines
journalctl -u myservice -n 50

# Since last boot
journalctl -u myservice -b

# Time range
journalctl -u myservice --since "1 hour ago"
journalctl -u myservice --since "2025-08-01" --until "2025-08-02"

# Without paging (pipe to grep etc.)
journalctl -u myservice --no-pager | grep error

Quick example: a minimal custom service

# /etc/systemd/system/myapp.service
[Unit]
Description=My application
After=network.target

[Service]
Type=simple
User=myapp
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now myapp

--now enables and starts in one step. Useful shortcut.