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.
Top comments (0)