॥ श्री ॥

Filesystem Structure

Architecture 2026-08-28

Shanios uses Btrfs with a sophisticated subvolume layout housed in a single Btrfs partition (plus a 1 GB FAT32 ESP). The design separates the immutable OS from all persistent state, so updates and rollbacks are atomic and data survives every slot switch.

Subvolume Layout

SubvolumeMount PointPurpose
@blue / @green/Root filesystems for blue-green deployment — alternates as active/standby. Mounted read-only by dracut; never in fstab.
@root/rootRoot user home — persists across slot switches
@home/homeUser data and personal configurations
@data/dataOverlay storage and persistent service data (bind-mount source tree)
@nix/nixNix package manager store — shared across both slots
@log/var/logSystem logs across reboots
@cache/var/cachePackage manager cache
@flatpak/var/lib/flatpakFlatpak applications and runtimes — seeded at install time from the image's flatpak_subvol snapshot, so base runtimes are already present on first boot
@snapd/var/lib/snapdSnap package storage, revisions, and writable snap data — seeded the same way from snapd_subvol
@waydroid/var/lib/waydroidAndroid system images and data
@containers/var/lib/containersPodman/Docker container storage
@machines/var/lib/machinessystemd-nspawn containers
@lxc/var/lib/lxcLXC containers
@lxd/var/lib/lxdLXD container and VM storage
@libvirt/var/lib/libvirtVirtual machine disk images (nodatacow)
@qemu/var/lib/qemuBare QEMU VM disk images (nodatacow)
@swap/swapSwap file container (nodatacow)

Unlike the other subvolumes (created empty with btrfs subvolume create), @blue and @green are seeded from the installed OS image: the installer extracts the base image into a subvolume named shanios_base, takes a read-only snapshot of it as @blue, then takes a read-only snapshot of @blue as @green — so both slots start out byte-identical — and deletes shanios_base. Every later shani-deploy update follows the same pattern against the candidate (inactive) slot: it snapshots the current candidate as a timestamped @<slot>_backup_<timestamp> safety copy, btrfs receives the new OS image into a temp_update/shanios_base subvolume, deletes the old candidate, and snapshots the freshly received image into @<slot> (read-only) — so a deploy never touches the booted slot and a failure mid-extraction leaves the candidate untouched.

The @data Subvolume

@data is the heart of Shanios persistence. Its internal structure:

/data/
├── overlay/
│   └── etc/
│       ├── upper/     ← your /etc changes are stored here
│       └── work/      ← kernel OverlayFS work directory
├── varlib/            ← bind-mount sources for /var/lib/*
│   ├── NetworkManager/
│   ├── bluetooth/
│   ├── caddy/
│   ├── cloudflared/
│   ├── cups/
│   ├── dbus/
│   ├── firewalld/
│   ├── fontconfig/
│   ├── gdm/ / sddm/
│   ├── pipewire/
│   ├── polkit-1/
│   ├── samba/ / nfs/
│   ├── sshd/
│   ├── systemd/
│   ├── tailscale/
│   ├── tpm2-tss/
│   ├── upower/
│   ├── fwupd/
│   ├── fprint/
│   ├── AccountsService/
│   ├── boltd/
│   ├── sudo/
│   ├── appimage/
│   ├── fail2ban/
│   ├── restic/ / rclone/
│   └── ...
├── varspool/          ← bind-mount sources for /var/spool/*
│   ├── cron/
│   ├── at/
│   ├── anacron/
│   ├── cups/
│   ├── samba/
│   └── postfix/
├── downloads/         ← cached OS images (used by shani-deploy)
├── current-slot       ← "blue" or "green"
├── previous-slot      ← the slot before the last deploy/rollback
├── boot-ok            ← written by mark-boot-success
├── boot_in_progress   ← written by mark-boot-in-progress, cleared on success
├── boot_failure       ← written by check-boot-failure on soft boot failure
├── boot_failure.acked ← written when user acknowledges failure dialog
└── boot_hard_failure  ← written by dracut hook if root mount fails

Persistent Bind Mounts from @data

Because /var is volatile (tmpfs via systemd.volatile=state), critical service state is bind-mounted from @data. Every bind mount uses nofail,x-systemd.after=var.mount,x-systemd.requires-mounts-for=/data so the system boots cleanly even if individual services are not installed.

System Core

Source (@data)Target
/data/varlib/dbus/var/lib/dbus
/data/varlib/systemd/var/lib/systemd
/data/varlib/fontconfig/var/lib/fontconfig

Networking

SourceTarget
/data/varlib/NetworkManager/var/lib/NetworkManager
/data/varlib/bluetooth/var/lib/bluetooth
/data/varlib/firewalld/var/lib/firewalld
/data/varlib/samba/var/lib/samba
/data/varlib/nfs/var/lib/nfs

Remote Access & VPN

SourceTarget
/data/varlib/caddy/var/lib/caddy
/data/varlib/tailscale/var/lib/tailscale
/data/varlib/cloudflared/var/lib/cloudflared
/data/varlib/geoclue/var/lib/geoclue

Display, Audio & Peripherals

SourceTarget
/data/varlib/gdm/var/lib/gdm
/data/varlib/sddm/var/lib/sddm
/data/varlib/colord/var/lib/colord
/data/varlib/pipewire/var/lib/pipewire
/data/varlib/rtkit/var/lib/rtkit
/data/varlib/cups/var/lib/cups
/data/varlib/sane/var/lib/sane
/data/varlib/upower/var/lib/upower

Auth & Security

SourceTarget
/data/varlib/fprint/var/lib/fprint
/data/varlib/AccountsService/var/lib/AccountsService
/data/varlib/boltd/var/lib/boltd
/data/varlib/sudo/var/lib/sudo
/data/varlib/sshd/var/lib/sshd
/data/varlib/polkit-1/var/lib/polkit-1
/data/varlib/tpm2-tss/var/lib/tpm2-tss

Hardware & Data Protection

SourceTarget
/data/varlib/fwupd/var/lib/fwupd
/data/varlib/fail2ban/var/lib/fail2ban
/data/varlib/restic/var/lib/restic
/data/varlib/rclone/var/lib/rclone
/data/varlib/appimage/var/lib/appimage

Scheduling & Spools

SourceTarget
/data/varspool/anacron/var/spool/anacron
/data/varspool/cron/var/spool/cron
/data/varspool/at/var/spool/at
/data/varspool/cups/var/spool/cups
/data/varspool/samba/var/spool/samba
/data/varspool/postfix/var/spool/postfix

Mount Options Reference

  • Root slots (@blue/@green) — not in fstab — mounted read-only by dracut via kernel cmdline
  • @root, @home, @data — the only subvolumes mounted without nofail; the system is not considered bootable without them
  • Every other subvolume (@nix, @log, @cache, container/virtualisation subvolumes, @swap) — uses nofail so the system boots cleanly even if the subvolume doesn't exist yet
  • *Subvolumes under /var/** (@log, @cache, and all container/virtualisation subvolumes) — additionally carry x-systemd.after=var.mount,x-systemd.requires=var.mount since /var itself is a tmpfs that must exist first
  • VM disk subvolumes (@libvirt, @qemu) and @swap — use nodatacow,nospace_cache (required for correctness and performance)
  • All other Btrfs subvolumes — use noatime,compress=zstd,space_cache=v2,autodefrag
  • All bind mounts — use bind,nofail,x-systemd.after=var.mount,x-systemd.requires-mounts-for=/data

Why noatime?

Writing an access timestamp on every file read would generate massive write traffic on a busy system. All subvolumes use noatime to prevent this — reducing SSD wear, improving battery life, and eliminating pointless write amplification.

Why systemd.volatile=state?

/var is a tmpfs cleared on every reboot. This keeps the OS truly stateless — log files, caches, and runtime state cannot accumulate across reboots and affect behaviour. The bind mounts from @data selectively restore only the service state that should persist.

Adding a New Persistent Service

If you self-host a service that needs state to survive reboots:

# 1. Create the backing directory in @data
sudo mkdir -p /data/varlib/myservice

# 2. Add a fstab bind mount
sudo nano /etc/fstab
# /data/varlib/myservice  /var/lib/myservice  none  bind,nofail,x-systemd.after=var.mount,x-systemd.requires-mounts-for=/data  0 0

# 3. Reload and mount
sudo systemctl daemon-reload
sudo mount /var/lib/myservice

Changes to /etc/fstab are captured by the OverlayFS overlay and survive every OS update.

See Also