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.
| Section | Purpose | Typical keys |
|---|---|---|
[Unit] | Identity, ordering, requirements, conditions | Description, Documentation, After, Before, Requires, Wants, BindsTo, PartOf, Conflicts, OnFailure, ConditionPathExists, StartLimitIntervalSec, StartLimitBurst |
[Service] | How the process runs | Type, ExecStart, ExecStop, ExecReload, Restart, RestartSec, User, Group, Environment, WorkingDirectory, all Protect*/Private* sandboxing, MemoryMax, CPUQuota, OOMPolicy |
[Install] | What systemctl enable does | WantedBy, RequiredBy, Alias, Also, DefaultInstance |
Two traps worth memorizing:
StartLimitIntervalSec=andStartLimitBurst=live in[Unit], not[Service], even though they only ever matter toRestart=.[Install]is inert until you runsystemctl enable. AddingWantedBy=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 9090Scalar 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 %isystemctl start worker@images.service
systemctl enable worker@{images,video}.service| Specifier | Expands to |
|---|---|
%i | Instance name, unescaped |
%I | Instance name, unescaped and path-decoded |
%n | Full unit name |
%N | Unit name without the suffix |
%p | Prefix, the part before @ |
%u | User name the unit runs as |
%h | Home directory of that user |
%t | Runtime directory, /run or $XDG_RUNTIME_DIR |
%S | State directory, /var/lib |
%f | Unescaped 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.serviceverify catches unknown keys, bad paths, and missing dependencies without touching the running system. Run it before daemon-reload on anything hand-written.