Mounting subvolumes
Mount a btrfs subvolume at a custom path when a person or service should see
one part of the pool as its own filesystem. Common examples are a friendlier
path under /home, or a media path like /var/lib/jellyfin/media.
How braid mounts the pool
braid mounts the btrfs top-level subvolume (subvolid=5) at
/mnt/storage by default. Treat that as the management mount: it is where you
create subvolumes, run btrfs commands, and manage the whole pool. Consumer
services do not need access to that mount root.
The subvol= mount idiom
btrfs can mount a subvolume directly with subvol=<path>. The btrfs docs
describe the important isolation property this way: “the parent directory is
not visible and accessible”, which is “similar to a bind mount”.
For braid, that means a service can see only movies at
/var/lib/jellyfin/media without needing permission to traverse
/mnt/storage.
Recipe: mount a subvolume at a custom path
Create the subvolume while the pool is unlocked:
sudo btrfs subvolume create /mnt/storage/movies
Find the btrfs filesystem UUID from braid status (look for the FSID:
line; the JSON form is braid status --json and the field is fsid):
sudo braid status
Add a native systemd mount unit to your NixOS configuration:
systemd.mounts = [{
what = "/dev/disk/by-uuid/<btrfs-fs-uuid>";
where = "/home/dan/my-movies";
type = "btrfs";
options = "subvol=movies,ro,noatime";
wantedBy = [ "braid-online.service" ];
bindsTo = [ "braid-online.service" ];
after = [ "braid-online.service" ];
}];
Field notes:
whatpoints at the btrfs filesystem UUID, not an individual LUKS disk.whereis the path where the subvolume should appear.type = "btrfs"selects the btrfs mount helper.optionsselects themoviessubvolume.rois optional but recommended for read-only consumers.wantedBystarts the mount whenbraid-online.serviceactivates afterbraid unlock.bindsTois the load-bearing lifecycle edge. It puts the mount unit inBoundBy braid-online.service, which is whatbraid lockstops before unmounting the pool.afterorders mount startup afterbraid-online.service, so the btrfs/dev/disk/by-uuidsymlink exists before systemd resolveswhat.
Rebuild and verify:
sudo nixos-rebuild switch
findmnt /home/dan/my-movies
systemctl show -P BoundBy braid-online.service
The escaped mount unit name, for example home-dan-my\x2dmovies.mount, should
appear in BoundBy braid-online.service.
subvol= vs bind mount
Both approaches can expose a subtree at another path. subvol= is the better
default for braid because it is conventional btrfs configuration, it mounts the
subvolume directly, and it does not require the consumer to traverse
/mnt/storage.
Use a bind mount only when the consumer already has permission to traverse the source mount and you need the same mounted data at multiple paths.
Why not fileSystems with x-systemd.requires?
fileSystems is fstab-shaped. systemd’s fstab options can express
Requires= and After=, but not an arbitrary BindsTo=braid-online.service
edge. Without BindsTo, the mount is not listed in
BoundBy braid-online.service, so braid lock will not stop it before
unmounting the pool. Use native systemd.mounts for lifecycle-bound subvolume
mounts. See ADR 018
for the lifecycle model.
Worked example: read-only access for Jellyfin
Create the media subvolume:
sudo btrfs subvolume create /mnt/storage/movies
Mount it where Jellyfin expects media:
systemd.mounts = [{
what = "/dev/disk/by-uuid/<btrfs-fs-uuid>";
where = "/var/lib/jellyfin/media";
type = "btrfs";
options = "subvol=movies,ro,noatime";
wantedBy = [ "braid-online.service" ];
bindsTo = [ "braid-online.service" ];
after = [ "braid-online.service" ];
}];
Grant Jellyfin read-only traversal on the subvolume contents:
sudo setfacl -R -m u:jellyfin:rx /mnt/storage/movies
sudo setfacl -R -d -m u:jellyfin:rx /mnt/storage/movies
Do not add jellyfin to storage. That would grant the daemon read-write
access across the whole pool. The ACL above scopes read access to one
subvolume, and the subvol= mount means Jellyfin does not need to traverse
/mnt/storage itself.
Bind Jellyfin to the mount unit:
services.jellyfin = {
enable = true;
openFirewall = true;
};
systemd.services.jellyfin = {
wantedBy = lib.mkForce [ "var-lib-jellyfin-media.mount" ];
bindsTo = [ "var-lib-jellyfin-media.mount" ];
after = [ "var-lib-jellyfin-media.mount" ];
unitConfig.ConditionPathIsMountPoint = "/var/lib/jellyfin/media";
};
Bind the service to var-lib-jellyfin-media.mount, not directly to
braid-online.service. That ensures Jellyfin starts only after its media path
is mounted. During braid lock, systemd stops Jellyfin first, then the
subvolume mount, then braid unmounts the management mount and closes LUKS.
The full triad pattern is the same lifecycle shape described in Sharing and permissions.
Verify:
sudo -u jellyfin ls /var/lib/jellyfin/media
sudo braid lock
systemctl is-active jellyfin.service
systemctl is-active var-lib-jellyfin-media.mount
Point the Jellyfin web UI at /var/lib/jellyfin/media. After braid lock,
both units should be inactive and the LUKS devices should be closed.
Offline mountpoint safety
braid seals the pool mountpoint immutable (chattr +i) while the pool is
offline, so a process writing /mnt/storage before the pool mounts fails with
EPERM instead of silently landing data on the root filesystem (which the pool
would then hide on mount). See
ADR 028.
This boot seal covers only the pool mountpoint (/mnt/storage). It has two
consequences for subvolume mounts:
- Subvolumes mounted under
/mnt/storageare inherently protected by the parent seal – the bare mountpoint is the sealed directory. This is the safe default; prefer it. - Subvolumes mounted at separate paths (like the
/var/lib/jellyfin/mediaexample above) are not auto-sealed. While the pool is offline thesystemd.mountsunit is stopped, leaving a bare directory at that path. The unit you wired withbindsTo = braid-online.servicedoes not write while offline, but any other process that writes the path while the pool is offline lands data on root and gets shadowed on the next mount – the same bug the boot seal fixes for/mnt/storage.
To protect a separate-path subvolume mountpoint, seal it manually with the explicit-path form while the pool is offline:
sudo braid seal-mountpoint /var/lib/jellyfin/media
This is the braid-native remedy (the appliance has no chattr on its PATH). It
reports a non-zero exit if it could not protect the path, so a failed seal is
visible. It is not self-healing – unlike the pool mountpoint, braid does not
re-seal these paths on every boot, and braid doctor does not probe them. Re-run
it after a reconfiguration that recreates the directory. To clear it later, use
braid seal-mountpoint --unseal <path>.