NixOS Flakes: Architecture for Reproducible System Configuration
NixOS Flakes replace mutable channels with a locked, reproducible configuration format. This architecture note covers the minimal design, trust boundaries, operational checks, failure modes, and when to reconsider the approach.
14 Oct 2025, 23:10 UTC

The Problem: Impurity in NixOS Configuration
Traditional NixOS configurations rely on the <nixpkgs> channel, a mutable global pointer that makes builds non-reproducible across machines and time. A configuration that builds today may fail tomorrow when the channel updates, and there is no single source of truth for the exact dependency graph. Flakes solve this by replacing ad-hoc expressions with a standardized, locked format that enforces hermetic evaluation and explicit dependency declarations.
Requirements
- Eliminate impurity: Remove dependence on untracked channels and external state.
- Reproducible builds: Ensure identical outputs given identical inputs, across any machine.
- Single source of truth: One repository defines system configurations, packages, and development shells.
- Auditable upgrades: Every dependency change is visible in version control.
Smallest Suitable Design
A minimal flake repository contains two files:
# flake.nix
{
description = "My NixOS configuration";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
};
outputs = { self, nixpkgs }: {
nixosConfigurations = {
myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
};
};
packages.x86_64-linux.default = self.nixosConfigurations.myhost.config.system.build.toplevel;
devShells.x86_64-linux.default = nixpkgs.mkShell { buildInputs = [ nixpkgs.git ]; };
};
}
# flake.lock (generated, commit this)
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1715000000,
"narHash": "sha256-...",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "abc123...",
"type": "github"
},
"original": {"owner": "NixOS", "repo": "nixpkgs", "type": "github"}
}
},
"root": "root",
"version": 7
}
The flake.lock file pins every input to a specific revision and content hash. Committing it to git makes the entire dependency graph auditable and reproducible.
Trust and Data Boundaries
Evaluation Sandbox
Flake evaluation runs in a restricted sandbox. Only declared inputs are fetched—either from binary caches (substituters) or from source. No ambient environment variables, filesystem paths, or network access beyond the declared inputs are available during evaluation.
Secret Management
The Nix store is world-readable. Secrets must not be placed directly in the flake or its outputs. Instead, inject them at activation time using tools like sops-nix or agenix:
# configuration.nix snippet with sops-nix
{ config, pkgs, ... }:
{
sops = {
defaultSopsFile = "/etc/secrets/secrets.yaml";
age.keyFile = "/run/keys/age-key";
};
# sops-nix decrypts secrets.yaml into /run/secrets/ at activation
systemd.services.my-service.environment = {
API_KEY = config.sops.secrets.api_key;
};
}
Rotate encryption keys regularly. The flake.lock file itself contains no secrets—only public revision metadata.
Operational Checks
Before deploying, run these verification steps in the flake repository root (requires nix-command and flakes experimental features enabled):
- Full output validation:
Runs all output tests, including NixOS module validation for everynix flake checknixosConfigurationsentry. Fails on evaluation errors, missing derivations, or failed tests. - Build verification:
Verifies the system closure builds successfully. Output appears innix build .#nixosConfigurations.myhost.config.system.build.toplevel./resultsymlink. - Test VM boot (optional but recommended):
Boots a lightweight VM to validate runtime behavior, kernel parameters, and service startup.nix build .#nixosConfigurations.test-vm.config.system.build.vm ./result/bin/run-*-vm - Atomic switch with rollback:
Performs an atomic switch: new generation becomes default, previous generation remains bootable. On failure, the bootloader entry is not updated.sudo nixos-rebuild switch --flake .#myhost
Permissions: nix flake check and nix build run as your user. nixos-rebuild switch requires sudo (or membership in the wheel group with appropriate sudoers configuration).
Failure Modes and Mitigations
| Failure Mode | Cause | Mitigation |
|---|---|---|
| Evaluation error | Broken input (e.g., upstream flake API change) | Pin inputs in flake.lock; update deliberately with nix flake update |
| Substitution failure | Binary cache missing derivations | Fallback to local build (automatic); configure additional substituters |
| Secret leakage | Misconfigured sops-nix/agenix keys |
Encrypt at rest; restrict key file permissions (chmod 600); audit with nix store verify |
| Rollback failure | Corrupted bootloader entries | Use ZFS/btrfs snapshots or ext4+snapper; test nixos-rebuild switch --rollback regularly |
| Slow evaluation | Large recursive flake inputs | Use flake-parts for modular structure; manually pin heavy inputs |
Conditions That Would Change the Design
- Nix language version upgrade (e.g., Nix 3.0 with breaking changes): May require migration of flake schema and module system.
- Secret management paradigm shift: Adoption of HashiCorp Vault or cloud KMS integration would move secret injection out of the activation path.
- Non-NixOS targets: Supporting macOS (nix-darwin), containers, or remote builders often splits
outputsinto separate repositories or introducesflake-partsfor shared library code. - Team workflow changes: Monorepo vs. multi-repo trade-offs affect whether
nixosConfigurations,packages, anddevShellslive together or are published as separate flakes.
Practical Verification Checklist
After any change to the flake repository:
- Run
nix flake check— must pass without warnings. - Inspect
flake.lockfor unexpected floating dependencies (entries withoutrevornarHash). - Build the target configuration:
nix build .#nixosConfigurations.myhost.config.system.build.toplevel. - Deploy with
sudo nixos-rebuild switch --flake .#myhost. - Verify rollback works:
sudo nixos-rebuild switch --rollbackand confirm the previous generation boots.
Limitations
- Requires Nix 2.4+ with
experimental-features = [ "nix-command" "flakes" ]innix.conforNIX_CONFIG. - Older NixOS channels (pre-21.11) lack flake support entirely.
- Large monorepos with many recursive flake inputs can suffer evaluation latency;
flake-partsor manual input pinning mitigates this. - Secrets in the store are readable by all users—encryption at rest is non-optional.
Diagram Labels
- flake.nix → inputs (pinned)
- flake.lock → committed to git
- nix flake check → validates outputs
- nixos-rebuild switch → atomic with rollback
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.