NixOS Generations: Atomic Upgrades With a Minimal Declarative Design
Architecture note on how NixOS uses an immutable Nix store and generation pointers to provide atomic upgrades, rollback, and clear trust boundaries for declarative system configuration.
18 Jul 2026, 07:53 UTC

The problem is safe upgrades without manual drift
Running a production host on NixOS means you want a change to be buildable, switchable, and reversible without editing live files. The useful takeaway is that NixOS achieves this with an immutable Nix store and a single generation pointer, not with in-place package managers.
Requirements for a declarative host
Atomicity: a new configuration must become active as a whole or not at all. Rollback must be bit-identical to a previous state. Reproducibility: the same Nix expression must build the same store paths on any machine. Trust separation: unprivileged edits to configuration.nix must not be able to write into the Nix store.
A minimal design needs a pure description of the system, a content-addressed store for artifacts, and a privileged activation step that only swaps pointers.
Smallest suitable design
The system state is expressed in /etc/nixos/configuration.nix as a pure function over the Nixpkgs attribute set. Packages, services, file systems and kernel parameters are declared, not executed imperatively.
Evaluation produces a derivation that builds a new generation under /nix/store/-nixos-system-. The generation contains the initrd, kernel, systemd units and profile scripts. Activation creates a new GRUB menu entry and updates the runtime symlink /run/current-system to the new store path. Because the store is read-only after build, switching is a symlink update plus service reload, which is effectively instantaneous.
Example declaration of a bounded change:
{ config, pkgs, ... }: { networking.hostName = ""; services.nginx.enable = true; environment.systemPackages = with pkgs; [ curl jq ];}Running the activation on the host as root or via sudo builds the derivation and switches the pointer. A typical check is:
sudo nixos-rebuild switchExpected check: /run/current-system now points to a new /nix/store path and nixos-version reports the new generation. Risk: service reloads can drop short-lived connections; use nixos-rebuild boot for a reboot-bound change.
Trust and data boundaries
The Nix store is immutable after build and owned by root. The Nix evaluator is pure and side-effect free, so a user-editable configuration.nix cannot write to /nix/store during evaluation. Only the privileged activation script, run as root, may create symlinks under /run and reload systemd.
Data that must remain mutable lives outside the store: /etc/nixos, /var, and user home directories. Secrets should not be in configuration.nix; use agenix or sops-nix to keep them out of the store derivation.
Custom kernel modules or firmware loaded at runtime break the boundary unless they are added via hardware.kernelPackages and hardware.firmware in configuration.nix so they are built into the store derivation.
Operational checks
Build and preview without affecting the running system:
sudo nixos-rebuild testThis builds a new generation, starts services in a temporary profile and leaves /run/current-system unchanged. If it succeeds, the configuration is safe to switch.
Schedule next boot only:
sudo nixos-rebuild bootActivate now:
sudo nixos-rebuild switchVerify atomic upgrade by inspecting the pointer after switch:
readlink -f /run/current-systemVerify rollback by booting the previous GRUB entry and checking that nixos-version matches the earlier generation hash.
Failure modes
Garbage accumulation: old generations remain in the store until collected. Run nix-collect-garbage -d explicitly to reclaim space; otherwise disk fills.
Activation failure: if the activation script errors after the symlink is updated, systemd may start a mix of old and new units. The design mitigates this by building the whole generation before switching.
Non-declarative state: writing files under /etc outside configuration.nix or loading out-of-store firmware creates drift that is not captured by generations.
When the design changes
If you require mutable system state that must survive across generations without being declared, the pure model is insufficient. Add a separate state directory managed outside Nix.
If you need per-host runtime secrets that change frequently, move secret delivery out of the Nix derivation and into an activation-time fetch with proper access control.
If you depend on hardware that cannot be expressed via kernelPackages, you must accept reduced reproducibility or wrap the driver in a custom derivation that is part of the store.
Limitations: the Nix store grows monotonically without explicit GC, and the first build of a large configuration can be slow. Check result by confirming the generation count with nix-env --list-generations and ensuring /run/current-system points to the expected hash after each switch.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.