Day 58 — Impermanence
Day 58 — Impermanence
Stage VI · ~4h
Goal: Implement or consciously reject root filesystem impermanence using impermanence patterns—proving that unpersisted state disappears on reboot while /persist (or equivalent) keeps what you declare.
Impermanence is optional as a lifestyle. It is not optional to understand. Skipping still requires a written “why” for the Stage VI gate. Disk layouts pair with Day 57 Disko; secrets with Days 34–35. Target: NixOS 26.05.
Why this day exists
Snowflakes are not only packages—they are mutated roots: /etc edits, /var surprises, leftover packages. Impermanence makes the root a camping tent and /persist the shipping container. Combined with Disko btrfs/ZFS layouts, reinstalls become boring—and Day 88 disaster recovery gets a cleaner story.
Even if you never wipe root on a daily driver, the inventory habit (what must survive reboot?) is permanent skill.
Theory 1 — Model
boot
│
├─ tmpfs or fresh subvolume for /
├─ bind/mount persisted paths from /persist
└─ everything else is recreated by activation
| Path class | Examples | Strategy |
|---|---|---|
| Store | /nix |
Own subvolume/partition; never wipe carelessly |
| Persist | /persist/var/lib/postgres, ssh host keys |
Explicit list |
| Ephemeral | /var/cache, junk in /root |
Accept loss |
| Secrets | sops age keys | Persist carefully; permissions matter |
| Boot | EFI, kernels, generations | Separate story; do not “wipe /boot” casually |
What “disappears” means
On reboot, anything not in the persist list is gone if you actually wipe/reset root. Soft modes that only document policy without wipe are still useful—but do not claim impermanence until the reboot proof works.
Theory 2 — impermanence module (nix-community)
# flake input
inputs.impermanence.url = "github:nix-community/impermanence";
# follows nixpkgs when the project supports it{
imports = [ impermanence.nixosModules.impermanence ];
environment.persistence."/persist" = {
hideMounts = true;
directories = [
"/var/log"
"/var/lib/nixos"
"/var/lib/systemd/coredump"
{ directory = "/var/lib/tailscale"; mode = "0700"; }
];
files = [
"/etc/machine-id"
# ssh host keys often:
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_ed25519_key.pub"
];
users.alice = {
directories = [ "Downloads" ".ssh" ];
files = [ ".config/sops/age/keys.txt" ]; # only if you accept this layout
};
};
}Exact option paths can track module versions—confirm against current impermanence README when options shift.
neededForBoot
fileSystems."/persist".neededForBoot = true;If /persist is not available early enough, activation ordering fails in mysterious ways. Treat this as non-negotiable for real setups.
Theory 3 — btrfs rollback-on-boot pattern
Common blog/production pattern:
- Root subvolume
@
- On boot, optionally reset
@to empty snapshot
- Mount
@persistat/persist
- Impermanence binds declared paths
# Conceptual boot.initrd or systemd unit logic—use a maintained pattern
# (e.g. known btrfs wipe scripts in community flakes). Prefer copying a
# pattern you understand over opaque one-liners.| Layout piece | Role |
|---|---|
@ |
Ephemeral root |
@persist |
Declared state |
@nix |
Store (never auto-wipe) |
| EFI | Bootloader |
ZFS native datasets with mountpoint and rollback are an alternative school—pick one for the lab. Mixing half-understood btrfs blog posts with half-understood ZFS is how you reinstall for the wrong reason.
Theory 4 — What breaks first (expect it)
| Symptom | Cause | Fix |
|---|---|---|
| Cannot SSH after reboot | Host keys not persisted | Persist /etc/ssh/ssh_host_* |
| Machine ID churn | /etc/machine-id ephemeral |
Persist or accept |
| sops fails | Age key path wiped | Persist key path; mode 600 |
| Docker/podman volumes gone | State under /var/lib |
Persist selected dirs |
| User linger/services odd | systemd user state | Persist carefully or redesign |
| Tailscale identity reset | State dir wiped | Persist with tight mode |
| NetworkManager connections gone | Connection profiles ephemeral | Persist or declarative NM |
Iterate: wipe/reboot is a test suite, not a one-shot.
Theory 5 — Soft impermanence
Not ready to wipe root? Still gain discipline:
# Document-only policy
# - no manual /etc edits
# - all state dirs listed in inventory
# - weekly diff against expected pathsOr tmpfs for /tmp only—training wheels.
| Level | What you prove |
|---|---|
| A Full | Wipe + persist list + reboot proof |
| B Partial | Persist mounts without wipe; inventory honest |
| C Skip | Written architecture why mutable root is OK |
All three are valid if documented. Silent skip is not.
Theory 6 — Interaction with generations and secrets
Impermanence does not replace Nix generations. Store rollbacks and root wipes are different axes:
Generation rollback → previous system closure
Root wipe → empty / except binds from /persist
Secrets → must live on persist or offline media
| Scenario | Tool |
|---|---|
| Bad module activation | Generation rollback (Day 13 / Day 89) |
| Unknown files on root | Impermanence wipe |
| Lost age key | Offline backup; Day 88 pain if missing |
Theory 7 — Home Manager alignment
If HM manages ~/.config/... on an impermanent home, either:
- Persist the HM-managed paths you care about, or
- Accept regenerate-from-flake behavior, or
- Keep home on a persistent dataset
Mismatch symptoms: “my HM files vanished” after reboot. Align Day 25–27 layouts with today’s persist lists.
Worked example — minimal VM proof
# hosts/imperm-lab/configuration.nix fragments
{
imports = [
./disko-btrfs.nix
./persist.nix
];
fileSystems."/persist".neededForBoot = true;
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAA… lab"
];
services.openssh.enable = true;
system.stateVersion = "26.05";
}Proof protocol:
echo "ephemeral-should-die" | sudo tee /root/SCRATCH
echo "persist-me" | sudo tee /persist/PROOF
sudo reboot
# after boot:
cat /root/SCRATCH # should fail
cat /persist/PROOF # should workCapture both outcomes in notes. A proof that only tests the success path is incomplete.
Lab — multi-step
Suggested workspace notes: ~/lab/90daysofx/02-nixos/day58
Step 1 — Decide commit level
| Level | Action today |
|---|---|
| A Full | btrfs/ZFS + impermanence module + wipe proof |
| B Partial | Persist module without wipe; list future wipe |
| C Skip | Written architecture review why not (still do path inventory) |
Record choice in IMPERMANENCE.md.
Step 2 — State inventory
On current lab host:
sudo du -xh /var/lib | sort -h | tail -n 30
ls /etc/ssh
systemctl --user list-units --failed 2>/dev/null || trueClassify each important path: persist / regenerate / ignore.
Step 3 — Implement level A or B
Wire flake input; persist SSH keys + one service state dir you actually use. Set modes deliberately.
Step 4 — Proof reboot
Create SCRATCH vs PROOF files; reboot; record outcomes with timestamps.
Step 5 — Secrets path
Confirm sops-nix/agenix key path survives (or document regenerate procedure and offline backup location).
Step 6 — Rollback story interaction
Write 5 lines: store generations vs root wipes. Link Day 89 thinking early.
Step 7 — If Level C
Write one page: threat model, backup story (Day 69), why mutable root is acceptable for this host class, and what would trigger revisiting (e.g. multi-user, kiosk, fleet sameness).
Step 8 — Persist list review
Print effective list; remove anything that is “comfort mutable root.” Goal is intentional survival, not nostalgia.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
/persist not neededForBoot |
Boot fails ordering; set flag |
| Persisted too little | Iterate after each surprise reboot |
| Persisted too much | You rebuilt a mutable root in disguise |
| Home manager vs system paths | Align HM files with persist lists |
| LUKS + wipe scripts | Order of unlock vs mounts |
Forgetting /nix separation |
Do not wipe the store subvolume |
| SSH host key regenerate | Clients scream TOFU; persist keys or automate known_hosts |
| Age key mode 644 | Tighten; treat as root-equivalent |
Checkpoint
- Level A/B/C chosen and documented
- State inventory exists
- Reboot proof (or skip rationale) recorded
- SSH still works after reboot if remote
- Secrets path understood
- Generations vs wipe note written
Commit
git add .
git commit -m "day58: impermanence proof or documented skip"Write three personal gotchas before continuing.
Tomorrow
Day 59 — deploy-rs: push a lab config to a remote VM the boring way.