How systemd Unit Files Work
Introduction¶
If you run anything on a modern Red Hat, Rocky, Alma, Debian, or Ubuntu box,
systemd is the thing that starts it, keeps it running, and writes down what
happened when it didn't. Learning to write a unit file yourself — instead of
copying one and hoping — pays off the first time a service silently fails to
come back after a reboot.
By the end of this post you'll have a working service unit, understand what each section does, and know the handful of commands that tell you why a unit is in the state it's in.
Concepts¶
A unit is any object systemd manages. The common types:
| Suffix | Manages |
|---|---|
.service |
A process (daemon, one-shot script) |
.socket |
A listening socket that starts a service on demand |
.timer |
A schedule that triggers another unit (cron replacement) |
.target |
A named group of units (multi-user.target, graphical.target) |
.mount |
A filesystem mount point |
Units live in three places, in ascending priority:
/usr/lib/systemd/system/— shipped by packages. Don't edit these./etc/systemd/system/— your overrides and your own units.- Runtime/generated units.
A drop-in file at /etc/systemd/system/foo.service.d/override.conf lets you
change part of a packaged unit without replacing the whole thing.
Hands-on¶
A minimal service¶
Create /etc/systemd/system/hello.service:
[Unit]
Description=Hello world worker
# Start only after the network is actually configured.
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/hello-worker
Restart=on-failure
RestartSec=5
# Run as an unprivileged user you created for this.
User=hello
Group=hello
# A few cheap hardening options.
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/hello
[Install]
WantedBy=multi-user.target
The three sections:
[Unit]— metadata and ordering.After=controls order, not requirement;Wants=/Requires=control whether the dependency is pulled in.Wants=is the soft version — if the dependency fails, your unit still starts.[Service]— how to run the process.Type=simplemeans "the process I exec is the service". UseType=notifyif the program callssd_notify(READY=1),Type=forkingfor old-style daemons that background themselves, andType=oneshot(withRemainAfterExit=yes) for a script that runs once.[Install]— what happens onsystemctl enable.WantedBy=multi-user.targetcreates a symlink so the unit starts at boot.
Load it¶
# systemd caches unit files — re-read them after any change.
sudo systemctl daemon-reload
# Start now, and start on every boot.
sudo systemctl enable --now hello.service
Timers instead of cron¶
Create hello.timer next to the service:
[Unit]
Description=Run hello-worker every 15 minutes
[Timer]
OnCalendar=*:0/15
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true runs a missed job once after the machine comes back up.
Enable the timer, not the service: sudo systemctl enable --now hello.timer.
Verification and troubleshooting¶
systemctl status hello.service # state, main PID, last log lines
systemctl cat hello.service # the effective unit incl. drop-ins
systemctl show hello.service # every resolved property
journalctl -u hello.service -e # full logs, jump to the end
journalctl -u hello.service --since "10 min ago"
systemctl list-timers # next/last run of every timer
Common failure modes:
status=203/EXEC→ExecStartpath is wrong, not executable, or the interpreter (#!/usr/bin/env python3) isn't installed. Test the exact command as theUser=account.status=200/CHDIRorstatus=226/NAMESPACE→ a hardening directive is too strict.ProtectSystem=strictmakes the whole filesystem read-only except paths inReadWritePaths=; add the directory your service writes to.- Unit starts, then dies, no restart → you didn't set
Restart=. WithType=simple, systemd considers the service "started" the instant it execs, so a program that exits immediately looks like a clean start unlessRestart=on-failureis set. - Changes ignored → you forgot
daemon-reload. - Starts too early at boot → order it after what it needs
(
After=network-online.target,After=postgresql.service), and pull that dependency in withWants=. enabledid nothing → the unit has no[Install]section, so there's nothing for systemd to symlink.
To experiment without a file, systemd-run --unit=test --property=Type=simple /path/to/cmd
creates a transient unit you can inspect and then systemctl stop test.
Related tools and reading¶
- On-site: Crontab Generator for translating an existing crontab, and the Linux commands reference.
- Related posts: systemd timers vs cron, Finding what is using a port.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Linux & Sysadmin
resolv.conf and systemd-resolved
On modern distros /etc/resolv.conf points at a stub resolver managed by systemd-resolved. Editing it directly usually does nothing — here's the real model.
October 24, 2026Tuning Open File Limits on Linux
Four different limits control how many files a process can open. 'Too many open files' means you're hitting one specific one — here's how to find and raise it.
October 24, 2026ss and conntrack for Connection Debugging
ss shows you every socket and its send/receive queues; conntrack shows what the firewall is tracking. Together they explain most 'connection refused' and 'no buffer space' problems.
October 24, 2026