You add a secondary disk for /data, drop a line in /etc/fstab, and reboot. One of three things happens:

  1. Everything mounts cleanly.
  2. Boot stalls for the default start timeout while systemd waits on a missing UUID.
  3. 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 least What= and Where=. Optional: Type=, Options=, TimeoutSec=, DirectoryMode=, …
  • Local mounts order into local-fs.target. Network mounts (by FS type or _netdev) order into remote-fs.target and pull network-online.target.
  • nofail turns the dependency into a weak Wants= and drops ordering before the fs target — boot continues if the volume is gone.
  • noauto means 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 / --path so ., //, 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 /etc wins 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

  1. Wrong unit name — Where=/data/lyra but file named data.lyra.mount → unit never matches the path.
  2. Symlink mount point — rejected; generator tries to resolve fstab symlinks, native units do not accept symlink Where=.
  3. Missing disk without nofail — boot waits on device + mount timeouts.
  4. Services start against an empty under-mount — declare RequiresMountsFor= or order After= the mount unit.
  5. x-systemd.* in unit Options= — several fstab-only keys are silently ignored outside fstab.
  6. Literal % in options — write size=50%% not size=50% in unit files (tmp.mount does this).
  7. Nested automounts — unsupported; the inner pin defeats the outer.
  8. API filesystems — some kernel API mounts cannot be disabled via mount units (systemd.io/API_FILE_SYSTEMS).
  9. Volume manager paths — consider x-systemd.device-bound=false so 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-generator Discoverable 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, fstab x-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.mount on 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.