systemd Cheatsheet

Unit Files

Use this systemd reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

A Minimal .service File

This is the whole thing. Copy it, change three lines, and you have a managed daemon.

# /etc/systemd/system/myapp.service
[Unit]
Description=My application
Documentation=https://example.com/docs
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp
ExecStart=/usr/local/bin/myapp --port 8080
Restart=on-failure
RestartSec=5s

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

ExecStart= needs an absolute path, and it is not run through a shell. No pipes, no globs, no $VAR expansion beyond systemd's own. If you need shell features, call /bin/sh -c '...' explicitly.

What Each Section Is For

Putting a directive in the wrong section is the single most common reason a unit refuses to load. systemd will tell you Unknown key name and carry on.

SectionPurposeTypical keys
[Unit]Identity, ordering, requirements, conditionsDescription, Documentation, After, Before, Requires, Wants, BindsTo, PartOf, Conflicts, OnFailure, ConditionPathExists, StartLimitIntervalSec, StartLimitBurst
[Service]How the process runsType, ExecStart, ExecStop, ExecReload, Restart, RestartSec, User, Group, Environment, WorkingDirectory, all Protect*/Private* sandboxing, MemoryMax, CPUQuota, OOMPolicy
[Install]What systemctl enable doesWantedBy, RequiredBy, Alias, Also, DefaultInstance

Two traps worth memorizing:

  • StartLimitIntervalSec= and StartLimitBurst= live in [Unit], not [Service], even though they only ever matter to Restart=.
  • [Install] is inert until you run systemctl enable. Adding WantedBy= does nothing on its own.

Other unit types swap the middle section: [Timer] for .timer, [Socket] for .socket, [Mount] for .mount, [Path] for .path. [Unit] and [Install] are shared by all of them.

Load Path and Precedence

The first directory that contains a given unit name wins outright. Later directories are not merged, they are ignored.

/etc/systemd/system/          highest, yours
/run/systemd/system/          transient, lost on reboot
/usr/local/lib/systemd/system/
/usr/lib/systemd/system/      lowest, the distro's
systemd-analyze unit-paths          # the real ordered list on this box
systemd-analyze unit-paths --user
systemctl cat myapp                 # which file is actually loaded, plus drop-ins

User units use ~/.config/systemd/user/, then /etc/systemd/user/, then /usr/lib/systemd/user/.

Drop-In Overrides

Almost never copy a packaged unit into /etc. Add a drop-in instead, so package updates still reach you.

sudo systemctl edit nginx        # creates .../nginx.service.d/override.conf
sudo systemctl edit --full nginx # copies the whole file into /etc, use sparingly
sudo systemctl revert nginx      # undo either of the above

By hand, any .conf file in <unit>.service.d/ works, merged in alphanumeric order after the main file:

# /etc/systemd/system/nginx.service.d/10-limits.conf
[Service]
MemoryMax=2G
LimitNOFILE=65535

Resetting a list directive needs an empty assignment first. Directives that accumulate (ExecStart=, Environment=, After=, ReadWritePaths=, ExecStartPre=) append when set again, so a drop-in that only adds a new value ends up with both. Clear the list with a bare Key=:

[Service]
ExecStart=
ExecStart=/usr/local/bin/myapp --port 9090

Scalar directives such as Type=, User= or Restart= just overwrite, no reset line needed. Prefixing the filename with a number (10-, 20-) makes the merge order explicit.

Templates and Specifiers

A unit file with @ before the suffix is a template, instantiated with a value after the @.

# /etc/systemd/system/worker@.service
[Service]
ExecStart=/usr/local/bin/worker --queue %i
systemctl start worker@images.service
systemctl enable worker@{images,video}.service
SpecifierExpands to
%iInstance name, unescaped
%IInstance name, unescaped and path-decoded
%nFull unit name
%NUnit name without the suffix
%pPrefix, the part before @
%uUser name the unit runs as
%hHome directory of that user
%tRuntime directory, /run or $XDG_RUNTIME_DIR
%SState directory, /var/lib
%fUnescaped instance name prefixed with /
%%A literal %

Escape a path into a unit name with systemd-escape -p /srv/data.

Checking Before You Reload

systemd-analyze verify /etc/systemd/system/myapp.service
systemd-analyze verify myapp.service     # loads it plus everything it references
systemctl cat myapp.service
systemctl show -p Type -p ExecStart -p Restart myapp.service

verify catches unknown keys, bad paths, and missing dependencies without touching the running system. Run it before daemon-reload on anything hand-written.