You add a secondary disk for /data, drop a line in /etc/fstab, and reboot. One of three things happens:
- Everything mounts cleanly.
- Boot stalls for the default start timeout while systemd waits on a missing UUID.
- The machine comes up, but a critical path is empty and services write into the underlying directory instead of the volume you meant to attach.
fstab is still the right human-facing format. Under systemd it is not “just mount(8) at boot” — systemd-fstab-generator turns each entry into a native .mount (and sometimes .automount / .swap) unit with real dependency edges, timeouts, and failure semantics.
This guide is operational. Behavior comes from systemd.mount(5), systemd-fstab-generator(8), systemd-mount(1), systemd-escape(1), and stock units on systemd 257 (verified on Debian 13).
What a mount unit is (and is not)
| Piece | Job |
|---|---|
.mount unit | Mount one What= source on one Where= path under systemd job control |
systemd-fstab-generator | Translate /etc/fstab → .mount / .swap early at boot and on reload |
.automount unit | Optional on-demand / idle-unmount wrapper that activates a matching .mount |
systemd-mount(1) | Create transient .mount / .automount units from the CLI |
.path / .socket / .timer | Other activation types — paths, listeners, schedules — not filesystem mounts |
A mount unit does not replace:
- LUKS unlock (
crypttab/systemd-cryptenroll) or integrity layers (dm-verity,dm-integrity), - volume managers deciding which block device is the source,
- NFS-only “stop blocking boot with remote shares” workflows that live primarily on
.automount(covered separately).
It does replace “hope mount -a in a boot script is enough,” and it makes every mount visible to systemctl, the journal, and ordering against local-fs.target / remote-fs.target.
Mental model
- Unit name must match the mount point, escaped:
/data/backup→data-backup.mount. -
[Mount]needs at leastWhat=andWhere=. Optional:Type=,Options=,TimeoutSec=,DirectoryMode=, … - Local mounts order into
local-fs.target. Network mounts (by FS type or_netdev) order intoremote-fs.targetand pullnetwork-online.target. -
nofailturns the dependency into a weakWants=and drops ordering before the fs target — boot continues if the volume is gone. -
noautomeans nothing pulls the mount at boot unless another unit does. - Parent mounts imply child requirements automatically (hierarchy edges).
- Runtime mounts (USB sticks, manual
mount) still show up as mount units via/proc/self/mountinfo.
Prerequisites
systemctl --version | head -n 1 # systemd 257 (… Debian 13) man systemd.mount man systemd-fstab-generator man systemd-mount man systemd-escape systemctl list-units --type=mount --all --no-pager Stock examples already on a typical host:
systemctl cat tmp.mount # [Mount] # What=tmpfs # Where=/tmp # Type=tmpfs # Options=mode=1777,strictatime,nosuid,nodev,size=50%%,nr_inodes=1m Note the doubled %% in unit files — percent is a specifier character; a literal % must be written %% (systemd.mount(5)).
Naming: always use systemd-escape
Hand-rolling unit names is how you get not-found units and silent mismatches.
# Mount unit for a path systemd-escape -p --suffix=mount "/data/backup" # data-backup.mount systemd-escape -p --suffix=mount "/mnt/My Disk" # mnt-My\x20Disk.mount # Undo systemd-escape -u --path 'data-backup' # data/backup Rules that matter in practice (systemd.unit(5) / systemd-escape(1)):
- Leading
/is dropped; remaining/become-. - Use
-p/--pathso.,//, and trailing slashes are normalized first. - Mount units cannot be templated and cannot gain extra names via symlinks.
-
Where=must be an absolute path and must not be a symlink (the generator resolves fstab symlink targets for compatibility; native units refuse symlink destinations).
Anatomy of a .mount unit
# /etc/systemd/system/data-lyra.mount [Unit] Description=Lyra data volume Documentation=man:systemd.mount(5) # Optional explicit ordering beyond the defaults: # After=local-fs-pre.target # Before=local-fs.target [Mount] What=/dev/disk/by-uuid/11111111-2222-3333-4444-555555555555 Where=/data/lyra Type=ext4 Options=defaults,noatime # DirectoryMode=0755 # TimeoutSec=30s # ReadWriteOnly=no # LazyUnmount=no # ForceUnmount=no # SloppyOptions=no [Install] WantedBy=local-fs.target | Setting | Meaning |
|---|---|
What= | Device node, UUID/LABEL-style source, bind path, or tmpfs |
Where= | Absolute mount point; must match unit name |
Type= | Filesystem type (ext4, xfs, btrfs, nfs, tmpfs, …) |
Options= | Comma-separated mount options (specifiers expanded) |
TimeoutSec= | How long mount may run before SIGTERM/SIGKILL failure |
DirectoryMode= | Mode when systemd creates Where= (default 0755) |
ReadWriteOnly= | If true, do not fall back to read-only after RW failure (x-systemd.rw-only) |
LazyUnmount= | umount -l semantics on stop |
ForceUnmount= | umount -f (classic unreachable-NFS hammer) |
SloppyOptions= | Tolerate unknown options (mount -s) |
User= / Group= are not useful here: systemd invokes mount(8) with What and Where as UID 0 and does not re-read fstab inside that call.
fstab is still fine — learn the x-systemd knobs
Humans often prefer /etc/fstab. Tooling and image builders often prefer unit files under /etc. Precedence (systemd.mount(5)):
- Unit in
/etcwins over fstab. - fstab wins over units shipped under
/usr.
Generator: /usr/lib/systemd/system-generators/systemd-fstab-generator (also on daemon-reload).
Options that change boot behavior
| fstab option | Effect |
|---|---|
nofail | Wants= only; not ordered before local-fs/remote-fs; boot continues if mount fails |
noauto / auto | Do / do not pull from the fs target (x-systemd.automount overrides this pair) |
_netdev | Force “network mount” ordering even if the type looks local (iSCSI, etc.) |
x-systemd.device-timeout= | How long to wait for the backing device (fstab-only) |
x-systemd.mount-timeout= | How long the mount command may run (fstab-only → TimeoutSec=) |
x-systemd.requires= / x-systemd.wants= | Extra unit/path dependencies on the mount unit |
x-systemd.before= / x-systemd.after= | Extra ordering on the mount unit |
x-systemd.wanted-by= / x-systemd.required-by= | Custom pull-in; suppresses default fs-target deps (except umount.target) |
x-systemd.requires-mounts-for= / wants-mounts-for= | Path-based mount dependencies |
x-systemd.device-bound= | true → BindsTo= device; false → survive device vanish (volume managers) |
x-systemd.automount | Create companion .automount |
x-systemd.idle-timeout= | Automount idle unmount (TimeoutIdleSec=) |
x-systemd.makefs | Format if the device has no signature (systemd-makefs@.service) |
x-systemd.growfs | Grow FS to fill the block device after mount |
x-systemd.pcrfs | Measure FS identity into PCR 15 after mount (253+) |
x-systemd.rw-only | No RO fallback |
x-initrd.mount | Mount in initrd and keep through host lifetime |
Example — secondary data disk that must not stall boot if unplugged:
UUID=11111111-2222-3333-4444-555555555555 /data/lyra ext4 defaults,noatime,nofail,x-systemd.device-timeout=5s,x-systemd.mount-timeout=15s 0 2 Example — image appliance partition that should format once and grow forever after repart:
/dev/disk/by-partuuid/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee /var/lib/myapp ext4 defaults,x-systemd.makefs,x-systemd.growfs 0 2 x-systemd.makefs / growfs / device-timeout / mount-timeout / pcrfs are fstab-only — putting them in a unit file’s Options= is ignored. In unit files, use the native settings (TimeoutSec=, companion services, etc.).
Network mounts and the old NFS bg flag
Network FS types get remote-fs-pre.target → network-online.target → remote-fs.target edges. _netdev forces that path for “local-looking” network block devices.
Classic NFS bg is rewritten by the generator roughly as:
- prepend
x-systemd.mount-timeout=infinity,retry=10000 - append
fg,nofail
For on-demand remote shares, x-systemd.automount is usually clearer than bg.
Inspect what the generator actually built
systemctl daemon-reload # Generated unit text (when the unit is known) systemctl cat data-lyra.mount # Or peek at generator output directly ls /run/systemd/generator/*.mount 2>/dev/null systemctl show -p Where,What,Options,Type,TimeoutUSec,Requires,Wants,After,Before data-lyra.mount findmnt /data/lyra journalctl -u data-lyra.mount -b --no-pager Kernel cmdline escapes for one-off/extra mounts without editing fstab (systemd-fstab-generator(8), 254+):
systemd.mount-extra=/dev/sda1:/mnt/extra:ext4:rw,noatime Credential fstab.extra can carry additional fstab-format lines (254+).
Lab 1 — Local ext4 mount unit on a loop file
Safe on any host with free space under /var/tmp. No real disks required.
sudo mkdir -p /var/tmp/lyra-mount-lab /mnt/lyra-lab sudo fallocate -l 512M /var/tmp/lyra-mount-lab/disk.img sudo mkfs.ext4 -F -L lyra-lab /var/tmp/lyra-mount-lab/disk.img # Stable What= via loop + label after first attach, or mount the file via Type=ext4: UNIT=$(systemd-escape -p --suffix=mount /mnt/lyra-lab) echo "$UNIT" # mnt-lyra-lab.mount sudo tee /etc/systemd/system/mnt-lyra-lab.mount >/dev/null <<'EOF' [Unit] Description=Lyra lab ext4 mount (loop file) [Mount] What=/var/tmp/lyra-mount-lab/disk.img Where=/mnt/lyra-lab Type=ext4 Options=loop,defaults,noatime TimeoutSec=20s [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now mnt-lyra-lab.mount systemctl status mnt-lyra-lab.mount --no-pager findmnt /mnt/lyra-lab echo 'hello from lyra mount lab' | sudo tee /mnt/lyra-lab/hello.txt Stop and prove the unit owns the lifecycle:
sudo systemctl stop mnt-lyra-lab.mount findmnt /mnt/lyra-lab || echo 'unmounted' sudo systemctl start mnt-lyra-lab.mount cat /mnt/lyra-lab/hello.txt Lab 2 — nofail vs required: don’t stall boot on optional media
Simulate “device missing” without rebooting by pointing What= at a nonexistent UUID:
sudo tee /etc/systemd/system/mnt-lyra-optional.mount >/dev/null <<'EOF' [Unit] Description=Lyra optional mount (nofail-style) DefaultDependencies=no Conflicts=umount.target Before=local-fs.target umount.target After=local-fs-pre.target # Weak pull-in — same idea as fstab nofail: Wants=local-fs.target # Intentionally NOT: RequiredBy=local-fs.target / Before hard-require [Mount] What=/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000 Where=/mnt/lyra-optional Type=ext4 Options=defaults TimeoutSec=5s [Install] WantedBy=local-fs.target EOF sudo mkdir -p /mnt/lyra-optional sudo systemctl daemon-reload sudo systemctl start mnt-lyra-optional.mount echo exit:$? systemctl status mnt-lyra-optional.mount --no-pager || true journalctl -u mnt-lyra-optional.mount -n 20 --no-pager You should see a failed mount unit quickly (device timeout / mount failure), not a multi-minute hang. With fstab nofail, local-fs.target still reaches active even when that mount fails; without nofail, a missing required disk can hold the boot transaction until timeouts cascade.
Equivalent fstab form:
UUID=00000000-0000-0000-0000-000000000000 /mnt/lyra-optional ext4 defaults,nofail,x-systemd.device-timeout=5s,x-systemd.mount-timeout=5s 0 0 Lab 3 — Transient mounts with systemd-mount
For USB sticks, one-off images, and scripts, skip unit files:
# List candidate filesystems systemd-mount --list # Mount lab image to an explicit path (two-arg form: no probe required) sudo systemd-mount \ --type=ext4 \ --options=loop,noatime \ --description='Lyra transient lab' \ --collect \ /var/tmp/lyra-mount-lab/disk.img \ /mnt/lyra-transient findmnt /mnt/lyra-transient systemctl status "$(systemd-escape -p --suffix=mount /mnt/lyra-transient)" --no-pager # Automount + idle unmount (stays clean until first access) sudo systemd-umount /mnt/lyra-transient sudo systemd-mount -A --timeout-idle-sec=30s --collect \ --type=ext4 --options=loop \ /var/tmp/lyra-mount-lab/disk.img \ /mnt/lyra-auto # First access triggers the real mount: ls /mnt/lyra-auto # After idle, systemd detaches the backing FS again. # tmpfs helper (255+) sudo systemd-mount --tmpfs lyra-tmp /mnt/lyra-tmpfs sudo systemd-umount /mnt/lyra-tmpfs Useful flags from systemd-mount(1):
| Flag | Why |
|---|---|
--discover | Probe label/model; implied for single-arg form |
-A / --automount=yes | On-demand mount |
--timeout-idle-sec= | Idle unmount in automount mode |
| `--fsck=yes\ | no` |
--bind-device | Stop when backing device vanishes (removable) |
-G / --collect | Unload failed transient units aggressively |
--owner=USER | Add uid=/gid= where the FS supports it |
--no-block | Enqueue without waiting |
udev pattern from the man page for auto USB mounts:
ACTION=="add", SUBSYSTEMS=="usb", SUBSYSTEM=="block", ENV{ID_FS_USAGE}=="filesystem", \ RUN{program}+="/usr/bin/systemd-mount --no-block --automount=yes --collect $devnode" Dependencies services should declare
Prefer path-based deps over hard-coding generated unit names:
# in myapp.service [Unit] RequiresMountsFor=/data/lyra # or softer: # WantsMountsFor=/data/lyra That pulls the right .mount units (and parents) without fragile string coupling. Hierarchy still adds implicit Requires/After between nested mounts.
Failure modes worth memorizing
- Wrong unit name —
Where=/data/lyrabut file nameddata.lyra.mount→ unit never matches the path. - Symlink mount point — rejected; generator tries to resolve fstab symlinks, native units do not accept symlink
Where=. - Missing disk without
nofail— boot waits on device + mount timeouts. - Services start against an empty under-mount — declare
RequiresMountsFor=or orderAfter=the mount unit. -
x-systemd.*in unitOptions=— several fstab-only keys are silently ignored outside fstab. - Literal
%in options — writesize=50%%notsize=50%in unit files (tmp.mountdoes this). - Nested automounts — unsupported; the inner pin defeats the outer.
- API filesystems — some kernel API mounts cannot be disabled via mount units (
systemd.io/API_FILE_SYSTEMS). - Volume manager paths — consider
x-systemd.device-bound=falseso teardown does not race the VM stack.
Cleanup the labs
sudo systemctl disable --now mnt-lyra-lab.mount mnt-lyra-optional.mount 2>/dev/null || true sudo rm -f /etc/systemd/system/mnt-lyra-lab.mount /etc/systemd/system/mnt-lyra-optional.mount sudo systemctl daemon-reload sudo systemd-umount /mnt/lyra-transient /mnt/lyra-auto /mnt/lyra-tmpfs 2>/dev/null || true sudo umount /mnt/lyra-lab 2>/dev/null || true sudo rm -rf /var/tmp/lyra-mount-lab /mnt/lyra-lab /mnt/lyra-optional /mnt/lyra-transient /mnt/lyra-auto /mnt/lyra-tmpfs sudo systemctl reset-failed 'mnt-lyra-*' 2>/dev/null || true What this is not
- Not a deep dive on NFS automount idle unmounts as the primary workflow (
.automount+ remote shares). - Not LUKS/TPM unlock, multipath, LVM thin, or Btrfs send/receive.
- Not
systemd-gpt-auto-generatorDiscoverable Partitions policy (sibling of fstab generation). - Not tmpfiles path creation policy (
systemd-tmpfiles) — mounts attach filesystems; tmpfiles lays out directories inside them.
References
-
systemd.mount(5)— mount unit settings, fstabx-systemd.*options, default dependencies -
systemd.automount(5)— companion on-demand units,TimeoutIdleSec= -
systemd-fstab-generator(8)— fstab → units,systemd.mount-extra=,fstab.extra, volatile modes -
systemd-mount(1)/systemd-umount— transient mount/automount helpers -
systemd-escape(1)— path → unit name escaping -
systemd.unit(5)—RequiresMountsFor=, specifiers, unit naming -
fstab(5),mount(8),umount(8) - Stock
tmp.mounton systemd 257 (Debian 13) - API file systems notes: https://systemd.io/API_FILE_SYSTEMS
Treat mounts as first-class units: named, ordered, timed, and observable. Keep fstab for the simple cases, reach for native .mount files and systemd-mount when tooling or transient workflows need a sharper edge — and put nofail + device timeouts on every volume that should not be allowed to hold the boot hostage.


