You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Setting up an ephemeral root with a /persist subvolume in NixOS follows the "erase your darlings" pattern. Because NixOS only requires /boot and /nix to boot and reconstruct the system, any state or configuration you want to retain across reboots must be explicitly saved in a persistent subvolume (e.g., /persist) and mounted or symlinked into the root filesystem.
Here is a step-by-step guide to configuring a Btrfs-based /persist setup in NixOS:
Create the Btrfs Subvolume Layout
Using a tool like Disko or manual partitioning, create the following Btrfs subvolumes on your drive/LUKS container: @root: The active root filesystem (wiped/recreated at every boot).
@root-blank: A read-only, completely empty Btrfs snapshot created right after initial formatting. @nix: Mounts to /nix (holds the Nix store and system configurations). @persist: Mounts to /persist (holds all opt-in persistent state). @log: Mounts to /var/log (optional, for log retention across reboots).
Configure Wipe-on-Boot in Initrd
Add a script in your NixOS configuration (boot.initrd.postResumeCommands or postDeviceCommands) that resets @root back to the blank snapshot before the root filesystem mounts:
boot.initrd.postResumeCommands = lib.mkAfter ''
mkdir /btrfs_tmp
mount /dev/disk/by-label/root /btrfs_tmp
if [ -e /btrfs_tmp/@root ]; then
Option 1: Move old root to a backup directory for 30-day retention/audit
mkdir -p /btrfs_tmp/old_roots
timestamp=$(date +%Y-%m-%d_%H:%M:%S)
mv /btrfs_tmp/@root "/btrfs_tmp/old_roots/$timestamp"
fi
Mount /persist with neededForBoot
In your fileSystems or Disko configuration, declare the /persist mount. Crucially, set neededForBoot = true so that the subvolume is mounted during early boot before activation scripts and secret decryption run:
fileSystems."/persist" = {
device = "/dev/disk/by-label/root";
fsType = "btrfs";
options = [ "subvol=@persist" "compress=zstd" ];
neededForBoot = true; # CRITICAL: Must be available early in the boot process!};
Configure the Impermanence Module
Enable the nix-community/impermanence module in your configuration.nix to handle bind-mounts and symlinks from /persist into the root filesystem.
Load-Bearing Paths That Must Be Persisted:
/var/lib/nixos: The most critical path. Persists UID/GID allocations. If omitted, user and group IDs will shift sequentially on every reboot, causing file ownership corruption.
/etc/machine-id: Required for journald log continuity.
/etc/ssh/ssh_host_ed25519_key & rsa_key: Required for SSH host identity.
Network Connections: /etc/NetworkManager/system-connections.
User Passwords: Point users.users..hashedPasswordFile directly into /persist (e.g., /persist/etc/shadow-passwords/username) or handle via sops-nix / agenix.
Essential Gotchas & Best Practices
Secret Managers (sops-nix / agenix): Point your secrets manager directly at the persisted key path (e.g., sops.age.sshKeyPaths =
["/persist/etc/ssh/ssh_host_ed25519_key" ];)
rather than the bind-mount target. Otherwise, a timing race during boot will cause secret decryption to fail because the key isn't mounted yet.
Immutable Users: Set users.mutableUsers = false; and ensure hashedPasswordFile or sops-nix's neededForUsers = true is configured. If you rely on imperative passwd commands without a persisted hash file, you will be locked out on the next reboot.
Backup LUKS Headers: If using full-disk encryption (LUKS2), keep an off-machine backup of your LUKS header. Re-provisioning an erased system requires only your pinned Nix flake, the /persist subvolume, and the LUKS header.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Setting up an ephemeral root with a /persist subvolume in NixOS follows the "erase your darlings" pattern. Because NixOS only requires /boot and /nix to boot and reconstruct the system, any state or configuration you want to retain across reboots must be explicitly saved in a persistent subvolume (e.g., /persist) and mounted or symlinked into the root filesystem.
Here is a step-by-step guide to configuring a Btrfs-based /persist setup in NixOS:
Using a tool like Disko or manual partitioning, create the following Btrfs subvolumes on your drive/LUKS container:
@root: The active root filesystem (wiped/recreated at every boot).
@root-blank: A read-only, completely empty Btrfs snapshot created right after initial formatting.
@nix: Mounts to /nix (holds the Nix store and system configurations).
@persist: Mounts to /persist (holds all opt-in persistent state).
@log: Mounts to /var/log (optional, for log retention across reboots).
Add a script in your NixOS configuration (boot.initrd.postResumeCommands or postDeviceCommands) that resets @root back to the blank snapshot before the root filesystem mounts:
boot.initrd.postResumeCommands = lib.mkAfter ''
mkdir /btrfs_tmp
mount /dev/disk/by-label/root /btrfs_tmp
if [ -e /btrfs_tmp/@root ]; then
Option 1: Move old root to a backup directory for 30-day retention/audit
mkdir -p /btrfs_tmp/old_rootstimestamp=$(date +%Y-%m-%d_%H:%M:%S)
mv /btrfs_tmp/@root "/btrfs_tmp/old_roots/$timestamp"
fi
Recreate a fresh @root from the blank snapshot
btrfs subvolume snapshot /btrfs_tmp/@root-blank /btrfs_tmp/@root
umount /btrfs_tmp'';
In your fileSystems or Disko configuration, declare the /persist mount. Crucially, set neededForBoot = true so that the subvolume is mounted during early boot before activation scripts and secret decryption run:
fileSystems."/persist" = {
device = "/dev/disk/by-label/root";
fsType = "btrfs";
options = [ "subvol=@persist" "compress=zstd" ];
neededForBoot = true; # CRITICAL: Must be available early in the boot process!};
Enable the nix-community/impermanence module in your configuration.nix to handle bind-mounts and symlinks from /persist into the root filesystem.
Load-Bearing Paths That Must Be Persisted:
/var/lib/nixos: The most critical path. Persists UID/GID allocations. If omitted, user and group IDs will shift sequentially on every reboot, causing file ownership corruption.
/etc/machine-id: Required for journald log continuity.
/etc/ssh/ssh_host_ed25519_key & rsa_key: Required for SSH host identity.
Network Connections: /etc/NetworkManager/system-connections.
User Passwords: Point users.users..hashedPasswordFile directly into /persist (e.g., /persist/etc/shadow-passwords/username) or handle via sops-nix / agenix.
Example Impermanence Declaration:
environment.persistence."/persist" = {
hideMounts = true;
directories = [
"/var/lib/nixos"
"/var/lib/systemd"
"/var/log"
"/etc/NetworkManager/system-connections"
];
files = [
"/etc/machine-id"
];
users.yourusername = {
directories = [
"Downloads"
"Documents"
".gnupg"
".ssh"
".local/share"
];
};
};
Secret Managers (sops-nix / agenix): Point your secrets manager directly at the persisted key path (e.g., sops.age.sshKeyPaths =
["/persist/etc/ssh/ssh_host_ed25519_key" ];)
rather than the bind-mount target. Otherwise, a timing race during boot will cause secret decryption to fail because the key isn't mounted yet.
Immutable Users: Set users.mutableUsers = false; and ensure hashedPasswordFile or sops-nix's neededForUsers = true is configured. If you rely on imperative passwd commands without a persisted hash file, you will be locked out on the next reboot.
Backup LUKS Headers: If using full-disk encryption (LUKS2), keep an off-machine backup of your LUKS header. Re-provisioning an erased system requires only your pinned Nix flake, the /persist subvolume, and the LUKS header.
All reactions