Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

systemd Unit Types

Overview

A unit is systemd’s universal configuration object: a name with a type suffix, a fragment file (or a programmatic origin), and a small state machine the manager drives. Everything the service manager touches is a unit — the daemons you care about (.service), the sockets they listen on (.socket), the filesystems they need (.mount), the schedules that run them (.timer), the hardware they need (.device), the cgroup sub-hierarchies they live in (.slice), and the grouping nodes that tie a boot together (.target).

This page is the catalogue: the state machine all units share, the full twelve-type matrix, a per-type section with the directives that matter, and the naming/escaping rules that bite everyone eventually. Unit file syntax, drop-ins and templating get their own treatment in unit-files.md.

The Unit State Machine

Every unit, regardless of type, moves through the same coarse states; the manager exposes finer-grained substates (e.g. dead, exited, running, listening, plugged, mounted, waiting) via systemctl show -p ActiveState,SubState:

inactive --> activating --> active --> deactivating --> inactive
                |                |
                v                v
             failed          failed   (on deactivation error / failed unit)
  • activating — start job in flight: the process is starting (service), the socket is binding (socket), the device is appearing (device).
  • active — the type-specific “ready” condition is met: process running, socket listening, mount mounted, device plugged.
  • deactivating — stop job in flight (ExecStop running, processes being killed).
  • failed — the unit terminated in error or a start-up condition failed. Clear with systemctl reset-failed (or automatically per CollectMode=).

What “active” means is entirely type-specific — that is the point of Type=notify on services, ListenStream= on sockets, and so on. The distinction between the coarse state and substates matters when scripting: systemctl is-active foo returning active does not tell you whether a oneshot exited (substate exited) or a daemon is running (substate running).

The Unit Type Matrix

TypeWrapsDefined byActivation triggerKey directives
.serviceA daemon or one-shot processUnit fileExplicit, socket/bus/path/timer, dependencyType=, ExecStart=, Restart=
.socketA listening socket (or set of)Unit fileConnection (if Accept=) or service startListenStream=, Accept=, Service=
.targetGrouping/synchronization pointUnit fileExplicit, dependencies, isolateWants=, Requires=, AllowIsolate=
.deviceA kernel deviceGenerated from udev eventsDevice appearing (udev)udev props SYSTEMD_WANTS=, SYSTEMD_ALIAS=
.mountA mount pointUnit file or fstab generatorExplicit, dependencies, automountWhat=, Where=, Type=, Options=
.automountAutofs trigger for a mountUnit file or fstab generatorFirst path accessWhere=, TimeoutIdleSec=
.timerA scheduled triggerUnit fileCalendar/monotonic elapseOnCalendar=, OnBootSec=, Persistent=
.swapA swap device/fileUnit file or fstab generatorExplicit, bootWhat=, Priority=, Options=
.pathA filesystem watcherUnit filePath condition satisfiedPathExists=, PathChanged=, Unit=
.sliceA cgroup hierarchy nodeUnit file (mostly)First contained unit startingSlice=, resource limits
.scopeExternally created process groupCreated via D-Bus/logind, no fileExternal call (systemd-run --scope, logind)set at creation time
.snapshotA saved unit stateLegacyLegacyremoved from upstream releases

Three structural facts to internalize: types are a contract with the manager, not just a name (a .socket unit’s whole purpose is to hold a socket so the service doesn’t have to); several types have no unit file at all (.device, .scope); and all types share the [Unit] and [Install] sections — the type-specific section ([Service], [Socket], …) only adds directives.

Service Units

.service units wrap processes: long-running daemons (Type=simple/exec/ notify/forking/dbus), one-shot actions (Type=oneshot), and everything between. The type’s start-up semantics, Exec* command lines, restart policies, watchdogs and the security sandbox are a full page of their own: service-units.md. Minimal shape for orientation:

[Unit]
Description=Metrics exporter
After=network.target

[Service]
ExecStart=/usr/local/bin/exporter --port 9100
Restart=on-failure

[Install]
WantedBy=multi-user.target

Socket Units

.socket units let systemd bind listening sockets (AF_INET/6, UNIX, FIFOs, and more) before the consuming service exists. Two payoff modes: service start-up is no longer serialized behind socket availability, and — with Accept=yes or on-demand activation — the service can be spawned only when the first connection arrives. Every socket-activated service gains automatic Wants=/After= on its socket, and the file descriptors arrive in the service as fds 3+ (SD_LISTEN_FDS_START) with LISTEN_FDS/LISTEN_PID in the environment. Full mechanics: socket-activation.md.

Target Units

.target units are synchronization points and grouping nodes; they run no process of their own. They are the runlevel replacement, the dependency anchor points (sysinit.target, multi-user.target), and the packaging mechanism for “start my whole stack” (myapp.target with Wants=myapp-api.service myapp-worker.service). The boot chain, runlevel compatibility and custom-target design are covered in targets-runlevels.md.

Timer Units

.timer units trigger other units on calendar times (OnCalendar=Fri 12:00) or monotonic offsets (OnBootSec=15min), with Persistent=yes to catch up missed runs after downtime (the cron anacron-style behavior), RandomizedDelaySec= to spread load, and list-timers for visibility. Calendar spec syntax, the Persistent catch-up model and the cron comparison live in timers.md.

Mount, Automount and Swap Units

.mount, .automount and .swap units are systemd’s take on what /etc/fstab describes — and in fact you usually do not write them: the fstab generator reads /etc/fstab at boot (and at daemon-reload) and creates the units for you. You can write native fragments, but the unit name must match the mount path (see the escaping section below), which is clunky; the fstab route plus x-systemd.* options is the idiomatic path.

The x-systemd.* fstab options that matter (parsed by the generator and documented in systemd.mount(5)):

fstab optionEffect
x-systemd.automountCreate an .automount instead of mounting at boot — the mount happens on first access
x-systemd.idle-timeout=60Unmount the automounted filesystem after 60 s idle
x-systemd.mount-timeout=10Per-mount timeout instead of the default 90 s
x-systemd.requires=unitsAdd a Requires= to other units (e.g. a tunnel service)
x-systemd.requires-mounts-for=/pathMount /path first (Requires+After for its mount unit)
x-systemd.wants= / x-systemd.after=Weaker pull-in / pure ordering from fstab
x-systemd.device-timeout=How long to wait for the backing device to appear
noauto, nofail, _netdevHonored as usual; noauto skips local-fs.target pull-in; nofail downgrades failure

The canonical network-filesystem recipe — do not let a dead NFS server block boot, but keep it transparent:

# /etc/fstab
nas:/export/media  /mnt/media  nfs4  noauto,x-systemd.automount,x-systemd.idle-timeout=60,_netdev  0  0

Boot proceeds without touching the network mount; the first ls /mnt/media triggers mnt-media.automount → mnt-media.mount. The same pattern is the answer to “my box hangs 90 s at boot on a dead NFS server”: noauto,x-systemd.automount plus x-systemd.mount-timeout=5 bounds the damage to the moment of first access.

Native .mount fragments exist for when fstab is not the right home (containers, initrd, generated systems):

# /etc/systemd/system/mnt-data.mount  (name MUST match the path)
[Mount]
What=/dev/disk/by-uuid/47d3-8f21
Where=/mnt/data
Type=ext4
Options=defaults,noatime

.automount pairs a Where= with the referenced mount unit; .swap units mirror [Swap] What=/Priority=/Options=. RequiresMountsFor= (in any unit) is the dependency shorthand for “this unit needs this path mounted” — it adds Requires+After on the relevant mount units automatically.

Device Units

.device units have no unit files — the manager creates them from udev events as the kernel discovers hardware, so their lifecycle mirrors the device’s. You configure their behavior from udev rules, using the systemd properties udev understands:

# /etc/udev/rules.d/99-backup-disk.rules
ACTION=="add", SUBSYSTEM=="block", ENV{ID_FS_LABEL}=="backup", \
  ENV{SYSTEMD_WANTS}="backup-sync.service", ENV{SYSTEMD_ALIAS}="/dev/disk/by-label/backup"
  • SYSTEMD_WANTS= — pull in a unit when the device appears: the standard “react to hardware” hook (hotplug service start).
  • SYSTEMD_ALIAS= — give the device unit a stable extra name (an alias symlink other units can reference).
  • TAG+="systemd" — ensure the device is exposed to systemd at all for devices outside udev’s default whitelist.

Mount units generated from fstab are automatically bound to their backing device (a BindsTo= + After= relationship), so unplugging the device stops and unmounts the mount — the mechanism behind “yanked USB disk” behaving sanely. Devices also participate in IgnoreOnIsolate= (default true for device units) so isolating targets does not rip devices out.

Path Units

.path units are inotify-driven triggers for other units — the native replacement for hand-rolled inotify scripts and cron-plus-timestamp hacks:

DirectiveFires when
PathExists=The path exists (checked on events and timer)
PathExistsGlob=Any path matches the glob
PathChanged=The path or a file inside was closed after writing (IN_CLOSE_WRITE)
PathModified=Writes are observed (IN_MODIFY) — fires more often than PathChanged
DirectoryNotEmpty=The directory has entries (spool-dir pattern)
[Unit]
Description=Process inbound invoices

[Path]
DirectoryNotEmpty=/var/spool/invoices
Unit=invoice-worker.service
MakeDirectory=no
# TriggerLimitIntervalSec=, TriggerLimitBurst= throttle event storms

[Install]
WantedBy=multi-user.target

The triggered unit is invoice-worker.service (default: the unit file with the same stem, invoices.path → invoices.service). The idiomatic pattern is a self-draining loop: the service empties the directory, and the path unit’s condition simply stays true (or refires) until the spool is empty. PathChanged vs PathModified in one line: use PathModified for “some activity happened” and PathChanged for “a file reached its final state”.

Slice and Scope Units

.slice units name nodes of the cgroup tree. The implicit root is -.slice; standard children are system.slice (normal system services), user.slice (all user sessions and user managers), machine.slice (containers/VMs registered with machined). A unit’s placement is chosen with Slice= (services default to system.slice); templated services get an automatic per-template slice (foo@.slice) so instances can be limited as a group. Because slices map directly onto cgroups, they are the carrier for resource control — CPU/IO/memory limits, accounting, oomd pressure rules: cgroups-resource-control.md.

.scope units are cgroups for processes systemd did not spawn: logind puts each login session in session-N.scope, container runtimes register their payloads as scopes, and systemd-run --scope wraps an arbitrary command. Scopes have no unit file and no start/stop semantics of their own — they exist so externally managed process trees get a supervised cgroup, a name, and resource settings applied at creation.

Unit Names, Aliases and Escaping

Rules worth knowing precisely:

  • A unit’s name must match its filename (minus directory); names are limited to 256 characters and may not contain /, shell glob characters (*?), or whitespace — hence escaping.
  • Aliases are plain symlinks to the unit file: [Install] Alias=foo.service creates one at systemctl enable time; systemctl follows alias chains when you ask for a unit by any of its names.
  • Template units end in @.service (etc.); instances are template@instance.suffix where the instance string is arbitrary and escaped — see unit-files.md.
  • Path-derived names (mounts, and unit names generated from paths) replace / with - and escape non-[A-Za-z0-9._] bytes as \xNN:
Inputsystemd-escape outputContext
/home/userhome-user.mountmount unit for the path
/mnt/My Diskmnt-My\x20Disk.mountspace escaped as \x20
/data/backups 2024data-backups\x202024plain path escaping (-p)
hello worldnotify@hello\x20world.servicetemplate instance via --template=notify@.service
$ systemd-escape -p --suffix=mount "/mnt/My Disk"
mnt-My\x20Disk.mount
$ systemd-escape --template=notify@.service "hello world"
notify@hello\x20world.service
$ systemd-escape --unescape 'mnt-My\x20Disk'
/mnt/My Disk        # (--unescape with -p)

The classic interview trap: writing etc-fstab.mount style names by hand without realizing the name is the path — a mount unit for /etc/fstab would be etc-fstab.mount, and a typo in the name means a mount unit for a path that does not exist. Always derive names with systemd-escape.

Where Unit Files Live (Quick Map)

Load precedence, highest first for the main fragment: /etc/systemd/system (admin) > /run/systemd/system (runtime) > /usr/lib/systemd/system (vendor). Drop-ins from all of these merge. Generators inject units under /run/systemd/generator*. The full ordered list, drop-in semantics and template details are the subject of unit-files.md; the one-liner for the exam is admin beats runtime beats vendor.

Interview Questions

Q: What is the difference between a .service, a .socket and a .timer for the same daemon?

They are three contracts with the manager around one daemon. The .socket holds the listening socket from boot (so connections queue in the kernel and start order stops mattering), the .service holds the process lifecycle (when it runs, how it restarts), and the .timer expresses when it should be triggered. Socket-activated services additionally get automatic Wants/After wiring between the pair, and systemctl start on the service starts the whole trio where relevant. The pattern replaces inetd-style super-servers with per-service units and no extra daemon.

Q: Why would you choose a .path unit over a cron job that checks for files?

Latency and correctness. A .path unit reacts to inotify events the moment the condition holds — no polling interval, no duplicate runs (the trigger unit runs once per condition transition), and no timestamp race. Cron is the right tool for wall-clock schedules; path units are the right tool for “something appeared/changed”. For spool directories, DirectoryNotEmpty= + a service that drains the directory is the canonical loop, with TriggerLimitBurst= guarding against event storms.

Q: How do mount units get created if nobody writes them?

The fstab generator. At boot and at daemon-reload, systemd-fstab-generator parses /etc/fstab and synthesizes .mount, .automount and .swap units under /run/systemd/generator, translating x-systemd.* options into native directives. A noauto,x-systemd.automount entry becomes an automount unit instead of a boot-time mount. Native fragments override generated ones, and hand-written .mount files must be named after the escaped mount path (/mnt/My Disk → mnt-My\x20Disk.mount).

Q: What is a .scope unit and who creates them?

A scope is a cgroup membership record for processes systemd did not start. There is no unit file; scopes are created programmatically — logind wraps every login session in session-N.scope, systemd-run --scope wraps an arbitrary command, and container runtimes register their process trees as scopes with machined. The scope gives those externally managed processes a supervised cgroup so resource limits, accounting and clean kill-on-shutdown apply uniformly to everything on the system.

Q: A service must not start before /var/data is mounted. Write the dependency.

RequiresMountsFor=/var/data in the [Unit] section — it adds the equivalent of Requires= + After= on every mount unit along the path. The longer manual form is Requires=var-data.mount plus After=var-data.mount, and forgetting the After= is the classic bug: requirement without ordering starts both units concurrently. For fstab-managed filesystems, the x-systemd.requires-mounts-for= option provides the same from fstab.

Q: What is the difference between a target and the SysV runlevel it replaces?

A runlevel was a global integer state the whole machine was in, maintained by init, with services attached via sequence-numbered symlinks. A target is just a unit — a named synchronization point others can Want/Require and order against — and many can be “active” simultaneously (multi-user, graphical, getty, custom app targets). There is no global mode switch; isolate merely starts one target and stops everything not pulled by it. The compatibility layer (runlevel(8), runlevelN.target aliases, telinit) maps the old integers onto specific targets.

References

Cross-References