Skip to content

systemd Unit Files: The Complete Directive Reference

Every systemd directive you will actually use, by section: [Unit] dependencies, Type= and Restart=, Exec prefixes, sandboxing, [Install], specifiers and version gaps.

●By 12 min read

systemd has hundreds of directives, and man systemd.directives lists every one of them with the page that documents it. That index is complete but not selective. This reference covers the directives that appear in real service files, grouped by section, with the defaults and the traps that the man pages mention only in passing.

If a unit is already failing, start with our guide to debugging systemd unit failures; for a worked end-to-end service, see running JupyterLab as a systemd service. To get a first draft quickly, the systemd unit generator writes the file and this page explains each line.

Where Unit Files Live and How Overrides Work

PathOwnerPrecedence
/etc/systemd/system/AdministratorHighest: wins over everything below
/run/systemd/system/Runtime, generated unitsLost at reboot
/usr/local/lib/systemd/system/Locally installed softwareAbove package units
/usr/lib/systemd/system/Distribution packagesLowest; never edit here (Debian also uses /lib before merged /usr)
~/.config/systemd/user/A user's own unitsFor systemctl --user

# The unit exactly as systemd sees it, including drop-ins

$ systemctl cat nginx.service

# Create a drop-in override (opens an editor, then reloads)

$ sudo systemctl edit nginx.service

# -> /etc/systemd/system/nginx.service.d/override.conf

# Replace a list-type directive: clear it first, then set it

[Service]

ExecStart=

ExecStart=/usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/alt.conf

# After editing any unit file by hand

$ sudo systemctl daemon-reload

$ systemd-analyze verify /etc/systemd/system/myapp.service

$ systemctl show myapp.service -p Restart -p User -p ExecStart

Empty assignment resets β€” most directives that accept a list (ExecStart=, Environment=, After=, conditions) are additive across drop-ins. An empty assignment such as ExecStart= clears the list; without it, a second ExecStart on a non-oneshot service is an error.

Drop-ins over copies β€” systemctl edit --full copies the whole unit into /etc, which then hides future package updates to it. Prefer a drop-in containing only what you change.

Find the real search path β€” systemd-analyze unit-paths prints every directory systemd reads on this machine, in order.

[Unit]: Description, Dependencies and Ordering

The most misunderstood fact in systemd: dependencies and ordering are separate. Requires= says "start that too"; it does not say "start it first". Without After=, both start in parallel.

DirectiveWhat it means
Description=Short human-readable name shown in status output and logs
Documentation=Space-separated URIs: http(s)://, man:, file:
Wants=Weak dependency: start the other unit too, but do not care if it fails
Requires=Strong: if the other unit fails to start (with After=), this one is not started; stopping it stops this one
Requisite=The other unit must already be active, or this start fails immediately
BindsTo=Like Requires=, and also stop this unit whenever the other one stops for any reason
PartOf=Stop and restart of the other unit propagate to this one (one direction only)
Conflicts=Starting this unit stops the other, and vice versa
After= / Before=Ordering only: wait for the other unit to finish starting
OnFailure=Units to start when this one enters the failed state (alerts, cleanup)
StartLimitIntervalSec= / StartLimitBurst=Rate limit for starts; defaults are 5 starts per 10s
DefaultDependencies=noDrop the implicit ordering against basic.target and shutdown (early-boot units only)

[Unit]

Description=Order queue worker

Documentation=https://wiki.example.internal/worker man:worker(8)

Wants=network-online.target

After=network-online.target postgresql.service

Requires=postgresql.service

OnFailure=notify-failure@%n.service

StartLimitIntervalSec=300

StartLimitBurst=5

# Run only where it makes sense; a failed Condition skips, it does not fail

ConditionPathExists=/etc/worker/worker.toml

ConditionVirtualization=!container

AssertPathIsDirectory=/srv/worker

network-online needs both β€” After=network-online.target alone does nothing unless something pulls the target in. Pair it with Wants=network-online.target.

Condition vs Assert β€” a false Condition* marks the start as skipped and is logged quietly. A false Assert* makes the start fail. Prefix a value with ! to negate it, and with | to make it one of several OR-ed triggering conditions.

StartLimit* moved β€” older guides put StartLimitInterval= in [Service]. Since systemd 230 the directives are StartLimitIntervalSec= and StartLimitBurst= in [Unit]. Reset a tripped limit with systemctl reset-failed unit.

[Service]: Type, Exec Lines, Restart and Timeouts

Type=systemd considers the service started when...Use for
simplethe process has been forked (default when ExecStart is set)Foreground daemons; startup errors are not detected
execthe binary has been successfully executed (systemd 240+)Like simple, but a missing binary fails the start
forkingthe initial process exits; set PIDFile= so the main PID is knownTraditional self-daemonising servers
oneshotthe process has exited; pair with RemainAfterExit=yesSetup scripts, one-off tasks, timer jobs
notifythe daemon sends READY=1 via sd_notifyDaemons with systemd support (nginx is not one; postgres is)
notify-reloadas notify; reload sends SIGHUP and waits for RELOADING=1 (systemd 253+)Newer notify-aware daemons
dbusthe name in BusName= appears on the D-BusD-Bus services
idlelike simple, but delayed until other jobs are dispatchedCosmetic: avoids console output clashing at boot
DirectiveNotes
ExecStart=The main command. One per service unless Type=oneshot. No shell: pipes, redirects and && are not interpreted
ExecStartPre= / ExecStartPost=Run before / after ExecStart; a failure aborts the start unless prefixed with -
ExecCondition=Exit 1-254 skips the unit without failing it; 255 fails it (systemd 243+)
ExecReload=Command for systemctl reload; often /bin/kill -HUP $MAINPID
ExecStop= / ExecStopPost=Graceful stop command; StopPost also runs after a failed start
Restart=no (default), on-success, on-failure, on-abnormal, on-watchdog, on-abort, always
RestartSec=Wait before restarting; default 100ms, which is why crash loops hit StartLimitBurst so fast
TimeoutStartSec= / TimeoutStopSec=Default 90s each; TimeoutSec= sets both; infinity disables
SuccessExitStatus=Extra exit codes or signals that count as clean, e.g. 143 for JVMs killed by SIGTERM
KillMode=control-group (default, kill everything), mixed (SIGTERM main, SIGKILL rest), process (main only)
WatchdogSec=Daemon must send WATCHDOG=1 within this interval or it is treated as hung

# Exec line prefixes

ExecStartPre=-/usr/bin/rm -f /run/myapp/stale.sock # "-": ignore a non-zero exit

ExecStartPre=+/usr/local/sbin/myapp-prepare # "+": run as root, no sandboxing

ExecStart=@/usr/bin/python3 myapp-worker /srv/app/w.py # "@": second word becomes argv[0]

# Variables: ${VAR} stays one argument, $VAR is split on whitespace

Environment="OPTS=--threads 4 --verbose" "PORT=8080"

ExecStart=/srv/app/bin/server $OPTS --port ${PORT}

# Need shell features? Ask for a shell explicitly

ExecStart=/bin/sh -c 'exec /srv/app/bin/server 2>&1 | logger -t server'

sponsored

[Service]: Users, Environment, Directories and Limits

DirectiveEffect
User= / Group=Run as this account; default is root
DynamicUser=yesAllocate a transient UID at start; implies ProtectSystem=strict, ProtectHome=read-only, PrivateTmp
SupplementaryGroups=Additional groups, e.g. ssl-cert on Debian to read private keys
WorkingDirectory=cwd for Exec lines; ~ means the User's home; prefix - to ignore a missing dir
Environment=KEY=value pairs; quote each pair that contains spaces
EnvironmentFile=Read KEY=value lines from a file; -/path ignores a missing file
StandardOutput= / StandardError=journal (default), null, file:/path, append:/path, truncate:/path, inherit
SyslogIdentifier=Tag used in the journal instead of the binary name
UMask=File creation mask; default 0022
StateDirectory=Creates /var/lib/NAME owned by User=, exported as $STATE_DIRECTORY
RuntimeDirectory=Creates /run/NAME at start and removes it at stop
CacheDirectory= / LogsDirectory=/var/cache/NAME and /var/log/NAME, same ownership rules
LimitNOFILE=Open-file limit; /etc/security/limits.conf does not apply to services
MemoryMax= / MemoryHigh=Hard cap (OOM kill) and soft throttle threshold for the unit's cgroup
CPUQuota=CPU time cap, e.g. 50% (half a core) or 200% (two cores)
TasksMax=Maximum processes and threads in the unit
Nice= / IOWeight= / CPUWeight=Scheduling priority and relative shares under contention

Distro conventions for env files β€” Debian and Ubuntu packages read /etc/default/NAME; RHEL and Fedora use /etc/sysconfig/NAME. Check systemctl cat to see which the packaged unit expects.

limits.conf is ignored β€” PAM limits apply to login sessions, not to services started by systemd. Raise limits with LimitNOFILE= in the unit or a drop-in, then check with cat /proc/$(pidof -s myapp)/limits.

Resource control and cgroup v2 β€” MemoryMax=, MemoryHigh= and the Weight= settings behave as documented on the unified cgroup v2 hierarchy, the default on RHEL 9, Debian 11 and later, and Ubuntu 21.10 and later. RHEL 8 boots with cgroup v1 by default, where some of them are mapped to older controls or ignored.

Do not chown in ExecStartPre β€” the *Directory= directives create and own their paths for you, and they remain writable even under ProtectSystem=strict.

Sandboxing Directives

These cost nothing at runtime and close off most of the filesystem and kernel from a compromised service. Add them one at a time and restart after each; when something breaks, the journal usually shows Read-only file system or Permission denied pointing to the cause. See our server hardening guide for where this fits in a wider baseline.

DirectiveEffectWatch out for
NoNewPrivileges=yessetuid binaries and file capabilities cannot raise privilegesServices that call sudo or ping via setuid
ProtectSystem=yes: /usr and /boot read-only; full: also /etc; strict: entire FS read-onlystrict needs ReadWritePaths= for anything not covered by *Directory=
ProtectHome=yes: /home, /root, /run/user inaccessible; read-only; tmpfsApps whose data lives under a home directory
PrivateTmp=yesPrivate /tmp and /var/tmpSharing files via /tmp with other services stops working
PrivateDevices=yesMinimal /dev without physical devicesGPU or serial-port access
PrivateNetwork=yesOnly a loopback interfaceAnything that needs the network
ProtectKernelTunables= / ProtectKernelModules=/proc/sys and /sys read-only; no module loadingRarely a problem for applications
ProtectControlGroups=yescgroup filesystem read-onlyContainer runtimes
ReadWritePaths= / ReadOnlyPaths= / InaccessiblePaths=Punch exceptions in either directionPaths must exist unless prefixed with -
RestrictAddressFamilies=e.g. AF_UNIX AF_INET AF_INET6Netlink users need AF_NETLINK
CapabilityBoundingSet= / AmbientCapabilities=Limit or grant capabilities, e.g. CAP_NET_BIND_SERVICE for port 443 as a non-root userAn empty bounding set removes everything
SystemCallFilter=@system-serviceAllow-list of syscalls typical services needPair with SystemCallArchitectures=native
MemoryDenyWriteExecute=yesNo writable and executable memoryBreaks JITs: Java, Node.js, some Python extensions

# Score a unit's exposure (0 = locked down, 10 = fully exposed)

$ systemd-analyze security myapp.service | tail -1

β†’ Overall exposure level for myapp.service: 2.1 OK :-)

# Try a directive without editing the unit

$ sudo systemd-run --pipe --wait -p ProtectSystem=strict -p ProtectHome=yes \

-p PrivateTmp=yes /usr/bin/touch /etc/test

/usr/bin/touch: cannot touch '/etc/test': Read-only file system

[Install], Templates and Specifiers

The [Install] section is read only by systemctl enable and disable, never at runtime. WantedBy=multi-user.target means "enable creates a symlink in multi-user.target.wants/", which is how the service starts at boot.

Directive / specifierMeaning
WantedBy= / RequiredBy=Target or unit that pulls this one in when enabled
Alias=Extra names created as symlinks on enable
Also=Units enabled and disabled together with this one, e.g. a matching .socket
DefaultInstance=Instance used when a template is enabled without one
%n / %NFull unit name / the same without the type suffix
%pPrefix: the part before @ in a template
%i / %IInstance name, escaped / unescaped
%H / %u / %hHostname / user name / user's home directory
%t / %S / %C / %L / %ERuntime, state, cache, logs and configuration roots (/run, /var/lib, /var/cache, /var/log, /etc)
%%A literal percent sign (needed in date formats)

# /etc/systemd/system/[email protected] β€” one unit file, many instances

[Unit]

Description=Queue worker %i

After=network-online.target

Wants=network-online.target

[Service]

Type=exec

User=worker

EnvironmentFile=/etc/worker/%i.env

ExecStart=/srv/worker/bin/worker --queue %i

Restart=on-failure

RestartSec=5s

StateDirectory=worker/%i

NoNewPrivileges=yes

ProtectSystem=strict

ProtectHome=yes

PrivateTmp=yes

[Install]

WantedBy=multi-user.target

$ sudo systemctl enable --now [email protected] [email protected]

$ systemctl status 'worker@*'

DistributionsystemdMissing from the tables above
RHEL 8 / Rocky 8 / Alma 8239Type=exec, ExecCondition=, notify-reload, OnSuccess=
Ubuntu 22.04249notify-reload
Debian 12, RHEL 9252notify-reload
Ubuntu 24.04255Nothing listed here

Check before you rely on a directive β€” systemctl --version shows the version, and systemd-analyze verify warns about directives this version does not know (they are otherwise ignored with only a log message).

Timers β€” scheduled units pair a .service with a .timer carrying OnCalendar= and Persistent=; the cron generator helps translate an existing crontab schedule first.

Frequently Asked Questions

What is the difference between Requires= and After= in systemd?

Requires= is a dependency: starting this unit also starts the other, and stopping the other stops this one. After= is ordering only: if both are being started, this one waits until the other has finished starting. They are independent, so for a real start-after dependency you normally set both.

Which Type= should I use for my service?

Use exec (or simple on systemd older than 240) for a program that stays in the foreground, notify if the daemon supports sd_notify, forking with PIDFile= for a traditional daemon that backgrounds itself, and oneshot with RemainAfterExit=yes for scripts that run and exit.

Why does my ExecStart pipe or redirect not work?

systemd does not run Exec lines through a shell, so |, >, && and globbing are passed to the program as literal arguments. Wrap the command in /bin/sh -c '...' when you need shell features, or use StandardOutput=append:/path for simple redirection.

How do I override a packaged unit file without editing it?

Run systemctl edit name.service, which creates a drop-in at /etc/systemd/system/name.service.d/override.conf and reloads systemd. Put only the changed directives there. To replace a list directive such as ExecStart=, first add an empty ExecStart= line, then the new value.

Why is my service stuck with start request repeated too quickly?

The unit hit its start rate limit, by default 5 starts within 10 seconds. With RestartSec at its 100ms default, a crashing service reaches it almost instantly. Fix the crash, raise RestartSec, tune StartLimitIntervalSec= and StartLimitBurst= in [Unit], and clear the state with systemctl reset-failed.

sponsored

Related Tools

See also