systemd service management cheat sheet
systemd is PID 1 on every mainstream distro, and two tools do nearly all the work: systemctl changes and inspects units, journalctl reads their logs. This sheet keeps the two side by side because the workflow is a loop — change something, then read what happened.
Mental model that prevents most confusion: start affects right now, enable affects the next boot — neither implies the other, and enable --now is the usual both-at-once. Unit suffixes (.service) are optional when unambiguous, so systemctl status nginx and systemctl status nginx.service are the same call.
systemctl: control and inspect units
| Task | Command | Notes |
|---|---|---|
| Start a service now | sudo systemctl start nginx.service | returns once started or failed; failures surface in status |
| Stop a service | sudo systemctl stop nginx.service | running processes receive SIGTERM, then SIGKILL after the timeout |
| Restart a service | sudo systemctl restart nginx.service | full stop plus start — connections drop; prefer reload when offered |
| Reload config without restart | sudo systemctl reload nginx.service | runs the unit's ExecReload; errors if the unit defines none |
| Reload if supported, else restart | sudo systemctl reload-or-restart nginx.service | convenient in scripts; check status afterwards to know which path ran |
| Status overview | systemctl status nginx.service | loaded/enabled state, MainPID, memory, and the last log lines |
| Is it running? | systemctl is-active nginx | prints active, inactive or failed; exit code works in scripts |
| Will it start at boot? | systemctl is-enabled nginx | enabled, disabled or static — see the list-unit-files row |
| Enable start at boot | sudo systemctl enable nginx | symlinks into multi-user.target.wants; does not touch the running state |
| Enable and start together | sudo systemctl enable --now nginx | the standard post-install one-liner |
| Disable start at boot | sudo systemctl disable nginx | leaves a currently running instance alone |
| Disable and stop together | sudo systemctl disable --now nginx | the clean decommission |
| Refuse every start attempt | sudo systemctl mask nginx.service | symlinks the unit to /dev/null — stronger than disable, blocks manual starts too |
| Undo a mask | sudo systemctl unmask nginx.service | restores normal startability |
| Loaded units on the system | systemctl list-units --type=service | shows loaded, active units; add --all for inactive ones too |
| Only failed units | systemctl --failed | the first stop whenever something breaks mysteriously |
| Installed unit files and boot state | systemctl list-unit-files --type=service | static = no [Install] section, pulled in by other units rather than enabled directly |
| Show the effective unit file | systemctl cat nginx | prints the original plus every drop-in override with file paths |
| Edit a drop-in override | sudo systemctl edit nginx | opens an editor on /etc/systemd/system/nginx.service.d/override.conf; daemon-reloads on save — restart the unit yourself |
| Edit the full original unit | sudo systemctl edit --full nginx | copies the vendor file to /etc/systemd/system; prefer overrides so upgrades still apply |
| Re-read unit files from disk | sudo systemctl daemon-reload | required after hand-editing units outside systemctl edit; restarts nothing |
| Clear a stale failed state | sudo systemctl reset-failed nginx | silences the red failed banner after the actual cause is fixed |
| Query individual properties | systemctl show nginx -p MainPID,ActiveState | comma-separated property list, script-friendly single-line output |
| Show the boot target | systemctl get-default | multi-user.target = server, graphical.target = desktop; set-default switches it |
| Scheduled timers | systemctl list-timers | cron's systemd counterpart; NEXT/LAST columns expose drift |
journalctl: read what happened
| Task | Command | Notes |
|---|---|---|
| Logs for one unit | journalctl -u nginx.service | oldest first; press G to jump to the end or add -e to start there |
| Follow one unit live | journalctl -f -u nginx.service | the tail -f equivalent across the journal; Ctrl+C exits |
| Logs in a time window | journalctl --since "2026-08-01 09:00" --until "2026-08-01 12:00" | quoted timestamps; today, yesterday and "1 hour ago" work too |
| Errors and worse | journalctl -p err | priority err and more severe: emerg, alert, crit, err — cuts noise hard |
| Current boot only | journalctl -b | -b -1 walks one reboot back — the post-crash question |
| Kernel messages | journalctl -k | the dmesg equivalent, with proper timestamps |
| Combined triage view | journalctl -xeu nginx.service | -x extra explanation, -e end of log, -u unit filter — start debugging here |
| Pipe-friendly tail | journalctl -n 50 --no-pager | last 50 lines straight to stdout, ready for grep or redirection |
| Journal disk footprint | journalctl --disk-usage | sum of archived plus active journal files |
| Shrink the journal now | sudo journalctl --vacuum-size=200M | also --vacuum-time=2weeks; make limits permanent via journald.conf (see FAQ) |
journalctl -u nginx -p warning --since yesterday is a valid, common compound. If logs vanish after reboot the journal is volatile — see the FAQ on /var/log/journal.Anatomy of a unit file
[Unit]
Description=My API service
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=api
WorkingDirectory=/opt/api
ExecStart=/usr/local/bin/api --port 8080
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
The directives that matter, by section
- [Unit] Description=
- human-readable name shown by status and list-units
- [Unit] After= / Before=
- pure ordering — start this unit before/after another; implies no dependency
- [Unit] Wants= / Requires=
- dependency strength: Wants pulls in but tolerates failure; Requires fails too when the dependency fails
- [Service] Type=
- simple (foreground, default), forking (daemonizes itself), oneshot (runs once), notify (signals readiness)
- [Service] ExecStart= / ExecReload=
- the start command; Reload is what systemctl reload triggers, conventionally a HUP via $MAINPID
- [Service] Restart=on-failure
- auto-restart on nonzero exit or signal; alternatives: always, no — plus RestartSec for the delay
- [Service] User= / WorkingDirectory=
- drop privileges and anchor relative paths; omitting User means root
- [Install] WantedBy=multi-user.target
- which target pulls the unit when enabled — multi-user for services, graphical adds display managers
FAQ
What is the difference between systemctl restart and reload?
restart stops the service completely and starts it fresh: all connections drop, caches rebuild, downtime equals startup time. reload asks the running process to re-read its configuration (via the unit's ExecReload directive) without stopping — sockets stay open. Reload only works if the unit defines ExecReload and the application supports live reconfiguration; nginx and sshd do, many apps do not. systemctl reload-or-restart tries reload and falls back to restart.
What is the difference between systemctl enable and start?
start acts on right now: launches the unit immediately and forgets it at reboot. enable acts on the next boot: creates a symlink in multi-user.target.wants so the unit starts automatically, touching nothing that is currently running. A freshly installed service typically needs enable --now — enable for future boots and start for this session in one command.
A service keeps failing — how do I debug it?
Three steps cover most cases. First systemctl status myapp.service: it shows the recent exit code and the last log lines inline. Then journalctl -xeu myapp.service for the full annotated story around the failure — -x adds explanations, -e jumps to the end. Common causes visible right there: bad paths (WorkingDirectory or ExecStart typos), permission problems from the User= account, or a port already held by another unit (ss -tulpn confirms). After fixing, systemctl reset-failed clears the stale state before restarting.
How do I shrink or limit journal disk usage?
Immediately: journalctl --disk-usage shows the current footprint, and sudo journalctl --vacuum-size=200M (or --vacuum-time=2weeks) trims it on the spot. Permanently: set SystemMaxUse=200M under the [Journal] section of /etc/systemd/journald.conf, then restart systemd-journald. Without a cap the journal grows toward 10% of the filesystem, which is rarely wanted on small volumes.
Why did my journalctl logs disappear after a reboot?
Because the journal was volatile — stored in /run/log/journal (a tmpfs) and wiped at boot. Persistence is controlled by journald's Storage setting (default auto): create /var/log/journal, run sudo systemctl restart systemd-journald, and logs survive reboots from then on. Most server distros ship persistent by default; containers and some minimal images do not.
Related tools
- IPv4 subnet calculator — break any CIDR block into network, range, broadcast and usable hosts.
- IP range to CIDR — turn an arbitrary address range into its minimal covering CIDR blocks.
- VLSM calculator — split a block into right-sized subnets by host requirements.