Impermanence#
Wipe the entire root filesystem on every boot. Everything starts clean, except the paths you explicitly mark as persistent. The system becomes honest about its state — if you didn’t declare it, it doesn’t survive.
This is a Level-3+ pattern in the four-level framing. It only makes sense once you have a real NixOS host with a real disk — internal drive, external SSD, or cloud VM installed via nixos-anywhere. You cannot meaningfully run impermanence inside NixOS-WSL (WSL owns the root filesystem) or inside a nixos-rebuild-build-vm throwaway (it already has no persistent state). It also depends on a declarative partition layout, which is what disko.md exists for — the two pages pair naturally.
You don’t need impermanence to enjoy NixOS, but it is the logical conclusion of “my system is a git repo of text files”: anything not in the repo either shouldn’t exist, or needs to be explicitly persisted. Reach for it once your configuration.nix + home.nix have stabilized and you want to know exactly what state your machine accumulates.
In this page#
Why impermanence#
Proves your config is complete. Any hidden state — logs, caches, half-written configs, unknown service data — vanishes on reboot. If something breaks when it disappears, you just found a gap in your
configuration.nix.Reinstalls are identical. Clone the repo, reformat, reboot: same system, no drift.
Security and forensics. An attacker who dropped a file in
/etc,/usr, or/tmploses it at reboot. Same for rootkits that forget the boot path.Less cruft. Uninstalling a service removes its config. No leftover dotfiles from tools you tried once. The system is always the sum of its declared state.
Clear mental model. Either a path is in
/persist, or it resets. No “maybe it’s cached somewhere” ambiguity.
The cost is upfront work: you must think about every piece of state and decide whether it deserves to persist. In practice that thinking is valuable — most people discover their machine was storing things they didn’t know about.
Two approaches#
Approach |
Root FS |
Complexity |
Flexibility |
|---|---|---|---|
Tmpfs root + impermanence |
RAM (tmpfs) |
Low |
Medium |
ZFS/Btrfs rollback |
Real FS, snapshot-restored |
Medium |
High |
Tmpfs root is easier to set up and requires no special filesystem. RAM-bounded, which means root FS caps at a few GB. Fine for a desktop/laptop; not great for servers with heavy logging.
Snapshot rollback keeps a real journaling FS (ZFS or Btrfs) and rolls back to a known-good snapshot before mount. No RAM limit, but requires ZFS/Btrfs familiarity and more careful dataset/subvolume planning.
Start with tmpfs. Migrate to ZFS rollback only if tmpfs bites you.
Approach 1 — tmpfs root + impermanence module#
Partition layout#
Typical layout for a single disk:
Partition |
Mount |
FS |
Purpose |
|---|---|---|---|
1 |
|
vfat |
ESP (EFI System Partition). |
2 |
|
ext4 |
The Nix store. Huge, cacheable, rebuildable. |
3 |
|
ext4 |
Everything that must survive a reboot. |
4 |
|
swap |
(Optional; for hibernate, must be ≥ RAM.) |
Root is not on disk — it is a tmpfs.
Minimal configuration.nix#
{
# Root on tmpfs — wiped on every boot.
fileSystems."/" = {
device = "none";
fsType = "tmpfs";
options = [ "defaults" "size=4G" "mode=755" ];
};
# Persistent storage for things we declare below.
fileSystems."/persist" = {
device = "/dev/disk/by-label/persist";
fsType = "ext4";
neededForBoot = true; # must be mounted before activation
};
# The Nix store itself.
fileSystems."/nix" = {
device = "/dev/disk/by-label/nix";
fsType = "ext4";
neededForBoot = true;
};
fileSystems."/boot" = {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
}
neededForBoot = true;on/persistis critical. Activation runs before fstab mounts, so any bind-mount from/persistwill fail silently unlessneededForBootforces it to mount in the initrd.
Wire in the impermanence module#
Flake:
{
inputs.impermanence.url = "github:nix-community/impermanence";
outputs = { self, nixpkgs, impermanence, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
impermanence.nixosModules.impermanence
./configuration.nix
];
};
};
}
Classic (no flakes):
# somewhere in configuration.nix
imports = [
(builtins.fetchTarball "https://github.com/nix-community/impermanence/archive/master.tar.gz" + "/nixos.nix")
];
Declare what persists#
environment.persistence."/persist" = {
hideMounts = true; # don't clutter `mount` and `df` output
directories = [
"/var/log" # system logs
"/var/lib/nixos" # UID/GID seeds, etc.
"/var/lib/bluetooth" # paired devices
"/var/lib/systemd/coredump"
"/var/lib/AccountsService" # user avatars
"/var/lib/NetworkManager" # known networks
"/etc/NetworkManager/system-connections" # saved wifi passwords
"/etc/ssh" # host keys
];
files = [
"/etc/machine-id" # systemd/journald identity
];
};
directories are bind-mounted; anything the system writes to them lands on /persist. files do the same for individual paths.
Approach 2 — ZFS / Btrfs snapshot rollback (“erase your darlings”)#
Your root filesystem is a real ZFS or Btrfs dataset. On every boot, before switch-root, a pre-boot hook restores a pristine empty snapshot of root. Persistent datasets/subvolumes (/home, /nix, /var/log, /etc/ssh, …) are mounted separately and untouched.
ZFS variant#
boot.initrd.postDeviceCommands = lib.mkAfter ''
zfs rollback -r rpool/root@blank
'';
Take the blank snapshot once, immediately after install, when root is in its desired empty state:
sudo zfs snapshot rpool/root@blank
Everything written to rpool/root between the snapshot and the next reboot gets erased.
Persistent data lives in separate datasets:
rpool/persist→/persistrpool/home→/homerpool/nix→/nixrpool/var-log→/var/log
None of these get rolled back. The impermanence module still helps declare bind-mounts if you prefer that style.
Btrfs variant#
Replace zfs rollback with a Btrfs subvolume swap:
boot.initrd.postDeviceCommands = lib.mkAfter ''
mkdir -p /mnt
mount -o subvol=/ /dev/disk/by-label/nixos /mnt
btrfs subvolume delete /mnt/root
btrfs subvolume snapshot /mnt/root-blank /mnt/root
umount /mnt
'';
Classic initrd vs systemd-initrd#
The boot.initrd.postDeviceCommands hook shown above is the classic initrd path. Modern NixOS increasingly defaults to systemd-initrd (enabled with boot.initrd.systemd.enable = true;), in which postDeviceCommands is ignored — you need a systemd unit that runs inside the initrd instead:
boot.initrd.systemd.enable = true;
boot.initrd.systemd.services.rollback = {
description = "Rollback BTRFS / ZFS root to blank snapshot";
wantedBy = [ "initrd.target" ];
after = [ "zfs-import-rpool.service" ]; # ZFS only
before = [ "sysroot.mount" ];
unitConfig.DefaultDependencies = "no";
serviceConfig.Type = "oneshot";
script = ''
zfs rollback -r rpool/root@blank
'';
};
For Btrfs, drop the after = [ "zfs-import-rpool.service" ]; line and put the subvolume-swap commands in script. Check boot.initrd.systemd.enable on your host before picking a style — mixing the two silently does nothing.
Pros and cons vs tmpfs#
No RAM ceiling — root can be arbitrarily large during a session.
Preserves real FS semantics (hardlinks, reflinks, inode numbers) which a few rare tools want.
More setup; dataset / subvolume planning becomes part of your config.
What to persist#
A starter list that covers most desktops:
environment.persistence."/persist" = {
hideMounts = true;
directories = [
# Logs and system state
"/var/log"
"/var/lib/nixos"
"/var/lib/systemd/coredump"
"/var/lib/systemd/timers"
# Networking
"/etc/NetworkManager/system-connections"
"/var/lib/NetworkManager"
"/var/lib/tailscale"
# Auth and SSH
"/etc/ssh"
# Hardware state
"/var/lib/bluetooth"
"/var/lib/upower"
"/var/lib/alsa"
# Desktop session data
"/var/lib/AccountsService"
"/var/lib/sddm"
"/var/lib/flatpak"
"/var/lib/cups"
# Service data (only the ones you actually run)
# "/var/lib/docker"
# "/var/lib/libvirt"
# "/var/lib/postgresql"
];
files = [
"/etc/machine-id"
"/var/lib/dbus/machine-id"
];
};
What absolutely must be there#
/etc/machine-id— systemd, journald, and many services key off this./etc/ssh/ssh_host_*_key— otherwise every SSH client on the planet yells about changed host keys after each reboot./var/lib/nixos— NixOS’s UID/GID state. Without it, users may get different numeric IDs across reboots, breaking file ownership./var/log— otherwise you lose the journal on every reboot, which defeats debugging./var/lib/NetworkManager//etc/NetworkManager/system-connections— known wifi networks and saved passwords.
Typically NOT persisted (deliberate)#
/tmp— by definition ephemeral./var/cache/*— caches, rebuild on demand./var/tmp— ephemeral./run/*— ephemeral by kernel convention.
Per-user persistence (home-manager)#
The impermanence repo ships a home-manager module. You choose per-user what to persist.
Flake:
inputs.impermanence.url = "github:nix-community/impermanence";
# in home.nix
{
imports = [ inputs.impermanence.homeManagerModules.impermanence ];
home.persistence."/persist/home/alice" = {
allowOther = true;
directories = [
"Documents"
"Downloads"
"Music"
"Pictures"
"Videos"
"Projects"
".ssh"
".gnupg"
".mozilla"
".config/kate"
".config/kdeconnect"
".config/obsidian"
".local/share/keyrings" # GNOME/KDE keyrings
".local/share/akonadi" # KDE PIM
".cache/mozilla" # optional — caches are cheap to rebuild
];
files = [
".bash_history"
".zsh_history"
".local/share/fish/fish_history"
];
};
}
allowOther = true;is needed for any directory that root or a service must read (e.g., keyrings, mail indexes).
You can also split by concern: one persistence block for “documents and downloads” that gets backed up aggressively, another for “caches” that doesn’t.
Pitfalls and gotchas#
neededForBoot = true;on/persist. Non-negotiable. Without it, activation starts before/persistis mounted and every bind-mount fails silently.Secret key bootstrap. If your decryption keys (age, SSH) live in
/persistand are needed at boot, ordering matters. agenix / sops-nix handle this, but think through the first-boot story.Database writes. Anything chatty (PostgreSQL, MySQL, Elastic) that you persist will hammer the real disk — plan size + IOPS accordingly. Tmpfs-root people sometimes put the data directory on a separate dataset entirely.
machine-id churn. If you forget to persist
/etc/machine-id, systemd regenerates it on every boot and the entire journal history becomes unreachable (it’s indexed by machine-id).bluetooth / keyring / GPG agents. These store per-user state under
~/.local/shareor~/.config. If you don’t persist the right subpaths, you re-pair your headphones or re-enter passphrases on every boot.Systemd timers with “persistent” schedules. These rely on
/var/lib/systemd/timersto remember when they last ran. Missing it means they all fire on every boot.allowOther = true;when sharing with system services. KDE wallet, Akonadi, certain file indexers need it.Disko + impermanence boot order. If you go whole-hog declarative with disko, be sure the disko layout sets labels that match
fileSystems.*.device.
Disaster recovery becomes trivial#
With impermanence and a git repo, reinstallation is:
Boot any NixOS installer — or your personalized live USB from this chapter.
Partition with disko from your flake:
nix run github:nix-community/disko -- --mode disko --flake .#myhost.Restore
/persistfrom backup if you want continuity, or leave empty for a truly fresh start.nixos-install --flake <your-repo>#myhost.Reboot.
The system returns bit-for-bit identical — because anything the old system relied on outside /persist was either (a) not state at all, or (b) already in your git repo.
Combined with nixos-anywhere, you can even skip the installer: run the install over SSH from any machine, wiping and rebuilding the target remotely.
Further reading#
github.com/nix-community/impermanence— the module’s source and README.Graham Christensen — Erase your darlings — the canonical blog post that popularized ZFS rollback.
github.com/nix-community/disko— declarative partitioning that pairs naturally with impermanence.
See also#
configuration.md — the module system and
fileSystemsoptions.disko.md — declarative partitioning; pairs with impermanence.
secrets.md — agenix/sops-nix, and how to persist their decryption keys so the system can boot.
tricks.md — live USB,
nixos-anywhere, and other reinstall tools that shine with impermanence.personal-setup.md — for
ssh.hostKeys, which must point at/persistwhen impermanence is in use.