Lesson 1 of 9 · 18m
Reading a systemd unit file like an operator
Where units live, what the three sections mean, and why systemctl edit beats editing files you don't own.
The alert says payments-worker is "failed" and the runbook says "restart it." Before you reach for systemctl restart, spend thirty seconds reading the unit — half of production restarts go sideways because nobody knew what Restart= or an Environment= override was already doing. Unit files are the source of truth for how a process actually runs, and they are plain text you can read faster than you can guess.
Where units live
systemd reads units from three directories, in increasing order of precedence:
/usr/lib/systemd/system/(Debian/Ubuntu:/lib/systemd/system/) — shipped by packages. Never edit these. Your change is silently overwritten on the next package update./run/systemd/system/— runtime units, gone on reboot./etc/systemd/system/— yours. Local units and overrides live here.
Anatomy of a unit
[Unit]
Description=Payments worker
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=notify
ExecStart=/opt/payments/bin/worker --config /etc/payments/worker.toml
Restart=on-failure
RestartSec=5s
User=payments
Environment=PAYMENTS_ENV=production
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target[Unit] is metadata and ordering. After=/Wants= control sequencing, not hard dependencies — Requires= is the hard version, and reaching for it first is usually a mistake. [Service] defines the process: Type= tells systemd how to know the service started (simple, notify, forking, oneshot), and ExecStart= is the command — and it is not a shell, so no pipes, redirection, or $(...) unless you wrap it in /bin/sh -c. Restart=on-failure plus RestartSec= gives bounded retries; StartLimitIntervalSec/StartLimitBurst in [Unit] bounds the restart loop. [Install] only matters at enable time: WantedBy=multi-user.target means "start on a normal boot."
Overrides — the mechanism to actually use
To change a packaged unit, don't copy the whole file to /etc. Use drop-ins:
systemctl edit payments-worker # writes /etc/systemd/system/payments-worker.d/override.conf
systemctl cat payments-worker # effective unit: package file + every drop-in, with filenames
systemctl revert payments-worker # throw away all local overridesExecStart is a multi-value directive — to replace it you must clear it first, or the daemon tries to run both lines:
[Service]
ExecStart=
ExecStart=/opt/payments/bin/worker --config /etc/payments/worker.toml --debugAfter editing: systemctl daemon-reload, then systemctl restart payments-worker.
Try this
Create /etc/systemd/system/heartbeat.service for a one-shot script:
[Unit]
Description=Heartbeat probe
[Service]
Type=oneshot
ExecStart=/usr/local/bin/heartbeat.shsystemctl daemon-reload && systemctl start heartbeat.service, then read it back with systemctl cat and check the run with systemctl status. Now write a drop-in via systemctl edit adding Environment=PROBE_TARGET=10.0.0.1 and confirm with systemctl show heartbeat -p Environment.
Pitfalls
- Editing the file in
/usr/liband losing the fix on the nextapt upgrade— the bug that ships. - Forgetting
daemon-reloadafter a manual file edit: systemd keeps running the old parsed unit. Type=forkingwithout a correctPIDFile=: systemd thinks the service died instantly and restarts it forever.- Putting shell syntax in
ExecStart:ExecStart=/app/run.sh > /var/log/outsilently writes nothing — redirect inside the script instead.