Persistence

Persistence Best Practices

Rules for using environment.persistence with impermanence without breaking NixOS activation.

The Rule

Never persist a directory that contains files created by NixOS activation (environment.etc, home.file, home.configFile, home.dataFile). Persist individual files instead.

Why

Impermanence bind-mounts /persist/<path> over /<path> via systemd mount units that run at local-fs.targetbefore NixOS activation. On first boot /persist/<path> is empty, hiding whatever activation places there.

Correct (file-level)

environment.persistence."/persist".files = [
  "/etc/ssh/ssh_host_ed25519_key"
  "/etc/ssh/ssh_host_ed25519_key.pub"
];

Wrong (directory-level)

# /etc/ssh/sshd_config is a symlink created by services.openssh
# via environment.etc. The empty bind mount hides it on first boot.
environment.persistence."/persist".directories = ["/etc/ssh"];

When directory persistence is fine

Directory persistence is safe for runtime data — files created by services at runtime, not by NixOS activation:

  • /var/lib — service databases, state
  • /var/log — log files
  • /var/spool — mail, print queues
  • User caches: .cache, .local/share, .local/state

These directories contain data that accumulates after boot and is never managed declaratively by NixOS.

neededForBoot and hideMounts

  • neededForBoot = true (set by persist.nix): Ensures /persist is mounted in initrd stage 1, before the real root switches. Required for persisted files needed by early boot (SSH host keys, machine-id).

  • hideMounts = true (set in each persist block): Tells GVFS to hide the mount point in file managers. Only affects desktop environments, has no effect on system behavior.

Real-world bug: SSH host keys

The modules/services/net/ssh.nix module persisted /etc/ssh as a directory. On first boot, the bind mount hid the sshd_config symlink that services.openssh creates via environment.etc. sshd failed with “sshd_config: No such file or directory”.

Fix: Persist only the host key files individually. sshd_config is a nix store symlink that doesn’t need persistence.

Module convention: gate on modules.persist.enable

All home.persistence and environment.persistence entries in feature modules must be conditional on config.modules.persist.enable. This ensures persistence is truly opt-in — when a host sets modules.persist.enable = false, no persistence entries are created and the impermanence module produces no uid/gid reassignment warnings.

# Simple list
home.persistence."/persist".directories =
  mkIf config.modules.persist.enable [
    ".cache/myapp"
  ];

# Wrapping an existing conditional
home.persistence."/persist".directories =
  mkIf config.modules.persist.enable (
    mkIf (!cfg.sandbox) [".config/myapp"]
  );

# Whole-attrset form
home.persistence."/persist" = mkIf config.modules.persist.enable {
  directories = [".config/myapp"];
  files = [".myapprc"];
};

The base modules/persist.nix handles two cases:

  • Enabled: maps home.persistenceenvironment.persistence with full directory set including /var/lib.
  • Disabled: force-clears environment.persistence via mkForce {} to suppress phantom entries from flake inputs.

Real-world bug: Podman registries.conf

The modules/services/virt/podman.nix module persisted /etc/containers as a directory, hiding registries.conf created by environment.etc. Same pattern, same fix: remove the directory persist since the file is NixOS-managed.