Unit Files¶
A unit file is the declarative description systemd uses for a service, socket, timer, mount or target: what to run, when, after what, and how to restart it. Most service failures in practice come from a few settings in these files, such as the path in ExecStart=, the Type=, ordering and overrides that never took effect.
Track: Core · Interview weight: High
Must-Know Facts¶
| Fact | Value | Verify with |
|---|---|---|
| Unit types | .service, .socket, .timer, .target, .mount, .automount, .path, .slice, .scope, .device, .swap | systemctl -t help |
| Sections | [Unit] (description, dependencies), [Service] / [Socket] / ... (type-specific), [Install] (enable) | systemctl cat <unit> |
| Search order | /etc/systemd/system overrides /run/systemd/system, which overrides /usr/lib/systemd/system | systemd-analyze unit-paths |
| Vendor units | /usr/lib/systemd/system (packages); never edit them | systemctl show -p FragmentPath |
| Drop-ins | /etc/systemd/system/<unit>.d/*.conf change single settings | systemctl show -p DropInPaths |
systemctl edit <unit> | Creates override.conf and reloads; --full copies the whole unit | systemctl cat <unit> |
| List-valued settings | ExecStart= with an empty value resets the list before a new value | systemctl cat <unit> |
daemon-reload | Required after any file change; otherwise systemd warns "changed on disk" | systemctl status <unit> |
Type= | simple (default), exec, forking, oneshot, notify, dbus, idle | systemctl show -p Type <unit> |
Restart= | no (default), on-failure, always, on-abnormal; RestartSec= delay | systemctl show -p NRestarts <unit> |
| Start limit | StartLimitBurst=5 in StartLimitIntervalSec=10s by default, then "Start request repeated too quickly" | systemctl reset-failed <unit> |
Wants= / Requires= | Pull in another unit; Requires= also fails or stops with it | systemctl list-dependencies <unit> |
After= / Before= | Ordering only; without them, units start in parallel | systemctl show -p After <unit> |
network-online.target | Needs Wants= and After= to wait for configured networking | systemctl show -p WantedBy network-online.target |
| Check a file | systemd-analyze verify <file> | systemd-analyze verify <file> |
| Overrides report | systemd-delta lists overridden and extended units | systemd-delta --type=extended |
Anatomy of a Unit¶
systemctl cat nginx.service | head -25
Output:
# /usr/lib/systemd/system/nginx.service
[Unit]
Description=The nginx HTTP and reverse proxy server
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
# Nginx will fail to start if /run/nginx.pid already exists but has the wrong
# SELinux context. This might happen when running `nginx -t` from the cmdline.
# https://bugzilla.redhat.com/show_bug.cgi?id=1268621
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
PrivateTmp=true
[Install]
WantedBy=multi-user.target
| Setting | Meaning |
|---|---|
After= / Wants= | Start after the network is configured, and ask for that target |
Type=forking | ExecStart= forks a daemon and exits; PIDFile= tells systemd the main PID |
ExecStartPre= | Commands that must succeed before the main command |
KillSignal=SIGQUIT | nginx treats QUIT as a graceful stop |
KillMode=mixed | SIGQUIT to the main process, SIGKILL to the rest after the timeout |
WantedBy= | The target that enable links the unit into |
Where Units Live¶
systemctl show nginx -p FragmentPath -p DropInPaths
Output:
FragmentPath=/usr/lib/systemd/system/nginx.service
DropInPaths=
systemd-analyze unit-paths lists the full search order; earlier directories win. A file with the same name in /etc/systemd/system replaces the vendor unit completely; a drop-in directory changes only the settings it names. Package updates overwrite /usr/lib/systemd/system and never touch /etc; Ubuntu 24.04 uses the same paths, while older Debian releases used /lib/systemd/system.
Overrides with Drop-ins¶
systemctl edit nginx opens an editor on /etc/systemd/system/nginx.service.d/override.conf and reloads on save. The same file can be written directly, followed by daemon-reload:
sudo mkdir -p /etc/systemd/system/nginx.service.d
printf '[Service]\nRestart=on-failure\nRestartSec=2\nLimitNOFILE=65536\n' | sudo tee /etc/systemd/system/nginx.service.d/override.conf >/dev/null
systemctl status nginx --no-pager | head -3
sudo systemctl daemon-reload; systemctl cat nginx | tail -6
systemctl show nginx -p Restart -p LimitNOFILE -p DropInPaths
Output:
Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk. Run 'systemctl daemon-reload' to reload units.
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabled)
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
Restart=on-failure
RestartSec=2
LimitNOFILE=65536
Restart=on-failure
LimitNOFILE=65536
DropInPaths=/etc/systemd/system/nginx.service.d/override.conf
Settings such as LimitNOFILE= apply only to processes started after the change, so the service needs a restart.
Replacing ExecStart¶
ExecStart= is a list. A drop-in that adds a second value to a non-oneshot service makes the unit invalid; an empty assignment clears the list first.
printf '[Service]\nExecStart=/usr/sbin/nginx -g "worker_processes 1;"\n' | sudo tee /etc/systemd/system/nginx.service.d/cmd.conf >/dev/null
sudo systemctl daemon-reload; sudo systemctl restart nginx
systemctl status nginx --no-pager | head -4
journalctl -b -u nginx --no-pager -o cat | grep -i "ExecStart" | tail -2
Output:
Failed to restart nginx.service: Unit nginx.service has a bad unit file setting.
See system logs and 'systemctl status nginx.service' for details.
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: bad-setting (Reason: Unit nginx.service has a bad unit file setting.)
Drop-In: /etc/systemd/system/nginx.service.d
└─cmd.conf, override.conf
nginx.service: Service has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.
nginx.service: Service has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.
The correct drop-in:
[Service]
ExecStart=
ExecStart=/usr/sbin/nginx -g "worker_processes 1;"
Service Types¶
Type= tells systemd when a service counts as started and which process is the main one.
Type= | Started when | Use for |
|---|---|---|
simple | Immediately after fork() | Default; foreground programs |
exec | After execve() succeeded | Foreground programs; reports a bad path as a start failure |
forking | The ExecStart= process exits | Classic daemons that background themselves |
oneshot | ExecStart= finished | Setup scripts; often with RemainAfterExit=yes |
notify | The program sends READY=1 via sd_notify | Programs with systemd support (sshd on Ubuntu, PostgreSQL) |
dbus | The program takes its D-Bus name | D-Bus services |
The difference between simple and exec shows with a missing binary:
sudo systemctl start exec-simple; echo rc=$?
sudo systemctl start exec-exec; echo rc=$?
Output:
rc=0
Job for exec-exec.service failed because the control process exited with error code.
See "systemctl status exec-exec.service" and "journalctl -xeu exec-exec.service" for details.
rc=1
Both units have ExecStart=/opt/missing/bin/app. With Type=simple, systemctl start returned success and the unit failed a moment later; deployment scripts that trust the exit code miss the failure.
A daemon that backgrounds itself needs Type=forking. Under Type=simple, systemd sees the main process exit, marks the unit finished and kills the rest of its cgroup:
sudo systemctl start fork; systemctl status fork --no-pager | sed -n "1,4p;/CGroup/,+1p"
sudo systemctl start simple-fork; sleep 1; systemctl status simple-fork --no-pager | sed -n "1,3p"; pgrep -a -f "sleep 600"
Output:
● fork.service - Forking daemon without PIDFile
Loaded: loaded (/etc/systemd/system/fork.service; static)
Active: active (running) since Wed 2026-09-16 19:12:16 UTC; 8ms ago
Invocation: d90c90e794214a319f35ae337f654710
CGroup: /system.slice/fork.service
└─4481 sleep 600
○ simple-fork.service - Daemon that backgrounds itself under Type=simple
Loaded: loaded (/etc/systemd/system/simple-fork.service; static)
Active: inactive (dead)
4481 sleep 600
Both units run ExecStart=/usr/bin/bash -c 'sleep 600 & exit 0'. Only the sleep of fork.service (PID 4481) is still running. static means the units have no [Install] section.
Restart and Start Limits¶
Restart=on-failure restarts after a non-zero exit, a signal or a timeout, and the start limit stops the loop. flaky.service runs ExecStart=/usr/bin/bash -c 'echo starting; exit 1' with Restart=on-failure and RestartSec=1 in [Service], and StartLimitIntervalSec=30 and StartLimitBurst=3 in [Unit]:
sudo systemctl start flaky; sleep 5
systemctl status flaky --no-pager
systemctl show flaky -p NRestarts -p Result
sudo systemctl start flaky
sudo systemctl reset-failed flaky; systemctl is-failed flaky
Output:
× flaky.service - Crashes on start
Loaded: loaded (/etc/systemd/system/flaky.service; static)
Active: failed (Result: exit-code) since Wed 2026-09-16 19:11:36 UTC; 1s ago
Duration: 103ms
Invocation: 8119cac66714457dad9afbf7e39a5618
Process: 4061 ExecStart=/usr/bin/bash -c echo starting; exit 1 (code=exited, status=1/FAILURE)
Main PID: 4061 (code=exited, status=1/FAILURE)
Sep 16 19:11:36 rocky-01 systemd[1]: flaky.service: Scheduled restart job, restart counter is at 3.
Sep 16 19:11:36 rocky-01 systemd[1]: flaky.service: Start request repeated too quickly.
Sep 16 19:11:36 rocky-01 systemd[1]: flaky.service: Failed with result 'exit-code'.
Sep 16 19:11:36 rocky-01 systemd[1]: Failed to start flaky.service - Crashes on start.
Result=exit-code
NRestarts=3
Job for flaky.service failed because the control process exited with error code.
See "systemctl status flaky.service" and "journalctl -xeu flaky.service" for details.
inactive
StartLimit* settings belong in [Unit], not [Service]. After the limit is reached, reset-failed clears the counter.
Restart=always hides crash loops
A service that restarts every few seconds looks active most of the time. Check NRestarts and the journal, and keep the start limit so that a broken release ends in failed, which monitoring can alert on.
Dependencies and Ordering¶
Wants= and Requires= decide which units start together; After= and Before= decide the order. Without ordering, both start in parallel.
app-web.service with only Wants=app-db.service:
sudo systemctl start app-web; journalctl -u app-db -u app-web --no-pager -o short-precise -n 20 | grep -E "starting|ready"
Output:
Sep 16 19:12:18.378030 rocky-01 bash[4539]: db starting
Sep 16 19:12:18.400186 rocky-01 echo[4541]: web starting
The web unit started 22 ms after the database began, without waiting for it. With After=app-db.service added:
sudo systemctl start app-web; journalctl -u app-db -u app-web --no-pager -o short-precise -n 30 | grep -E "starting|ready" | tail -3
systemctl show app-web -p Wants -p After -p Requires
Output:
Sep 16 19:12:19.698086 rocky-01 bash[4589]: db starting
Sep 16 19:12:21.699369 rocky-01 bash[4589]: db ready
Sep 16 19:12:21.789975 rocky-01 echo[4592]: web starting
Requires=sysinit.target system.slice
Wants=app-db.service
After=basic.target sysinit.target system.slice app-db.service systemd-journald.socket
| Directive | Pulls in the other unit | Fails if it fails | Stops with it | Orders |
|---|---|---|---|---|
Wants= | Yes | No | No | No |
Requires= | Yes | Yes (with After=) | Yes | No |
BindsTo= | Yes | Yes | Yes, also when it disappears | No |
Requisite= | No, must already be active | Yes | Yes | No |
After= / Before= | No | No | No | Yes |
Conflicts= | Stops the other unit | No |
Wait for the network with network-online.target
network.target only means the network stack is up. A service that must reach a remote host at start needs Wants=network-online.target and After=network-online.target, as the nginx unit has.
Common Errors¶
Failed to restart nginx.service: Unit nginx.service has a bad unit file setting.¶
Cause: a setting failed to parse, often a second ExecStart= added by a drop-in.
Fix: journalctl -b -u <unit> names the setting; add an empty ExecStart= before the new one, or run systemd-analyze verify.
Warning: The unit file, source configuration file or drop-ins of nginx.service changed on disk. Run 'systemctl daemon-reload' to reload units.¶
Cause: a unit or drop-in was edited without reloading, so systemd still uses the old version.
Fix: sudo systemctl daemon-reload, then restart the unit if the change affects the running process.
flaky.service: Start request repeated too quickly.¶
Cause: the unit failed more than StartLimitBurst times within StartLimitIntervalSec.
Fix: fix the cause in the journal, then sudo systemctl reset-failed <unit> and start it.
Interview Checkpoints¶
L1: Where do unit files live, and which location wins?
Say first: vendor units are in /usr/lib/systemd/system, runtime units in /run/systemd/system, and administrator units and drop-ins in /etc/systemd/system, which takes precedence.
Proof: systemd-analyze unit-paths; systemctl show -p FragmentPath -p DropInPaths <unit>
Follow-up: Why should the vendor file not be edited?
L1: What is the difference between Wants= and After=?
Say first: Wants= makes systemd start the other unit too; After= only orders the start. A dependency usually needs both.
Proof: with only Wants=, the journal shows both units starting within milliseconds.
Follow-up: When do you use Requires= instead of Wants=?
L1: What do Type=simple and Type=forking mean?
Say first: simple treats the started process as the service; forking expects it to fork a daemon and exit, and tracks the child.
Proof: a backgrounding script under Type=simple ends as inactive (dead) and systemd kills its child.
Follow-up: Why is Type=exec safer than simple?
L2: Raise the open-file limit of nginx without editing the vendor unit.
Say first: add a drop-in and restart.
Proof:
sudo systemctl edit nginx # [Service] LimitNOFILE=65536
sudo systemctl restart nginx
cat /proc/$(systemctl show -p MainPID --value nginx)/limits | grep 'open files'
Follow-up: Why does /etc/security/limits.conf not apply to services?
L2: Make a service restart on failure, but give up after 3 failures in 30 seconds.
Say first: set Restart= in [Service] and the start limit in [Unit].
Proof: StartLimitIntervalSec=30, StartLimitBurst=3, Restart=on-failure, RestartSec=1; systemctl show -p NRestarts.
Follow-up: How do you start it again after it hit the limit?
L3: An application service fails at boot but starts fine manually a minute later.
Say first: it probably starts before something it needs: the network, a mount or a database.
Proof: journalctl -b -u <unit> shows a connection or path error; systemd-analyze critical-chain <unit> shows its order; add Wants= and After= for network-online.target, the mount (RequiresMountsFor=) or the database unit.
Follow-up: Why is a sleep in ExecStartPre= the wrong fix?
L4: How does systemd know which processes belong to a service?
Say first: it starts every service in its own cgroup, and every child inherits the cgroup, so forked and double-forked processes are still tracked without PID files.
Proof: systemd-cgls -u nginx.service; cat /proc/<pid>/cgroup shows /system.slice/nginx.service.
Don't say: "systemd follows the PID file."
Related¶
- systemctl: commands that act on units
- Writing a Service: a unit built from scratch
- Process Lifecycle: daemons and double forks
- Signals:
KillSignal=and the stop sequence
Captured on Rocky Linux 10.2 (iximiuz Labs microVM, kernel 6.1.167), 2026-09.