Lock file discipline
Lock file discipline
Goal: Treat flake.lock as a deliberate pin, not a mysterious generated blob—update one input at a time, read diffs, and explain how locks interact with registries and multi-machine rebuilds.
Blind nix flake update on a production host is how “nothing changed except everything.” Prefer staged, selective updates.
Why this chapter exists
Flakes solve channel drift only if you respect the lock file. Teams fail when:
- Someone updates all inputs “because CI was green last week”
- Lock files are gitignored (never do this for apps/hosts)
- Two machines use different locks and argue about “Nix being unreproducible”
- Registries silently redirect
nixpkgsaway from the flake’s pin
Lock hygiene is foundational for every later input (Home Manager, sops-nix, flake-parts, …).
Theory 1 — What flake.lock actually pins
For each flake input, the lock records roughly:
| Field | Meaning |
|---|---|
type / owner / repo / url |
Where the input came from |
rev |
Exact git commit (or equivalent) |
narHash |
Content hash of the fetched tree |
lastModified |
Metadata for humans/tools |
Implication: two clones with the same lock should fetch the same input trees (modulo substituted vs rebuilt outputs later). Without the lock, nixos-26.05 is a moving branch tip.
Inputs vs follow-ons
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
home-manager.url = "github:nix-community/home-manager/release-26.05";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};follows means Home Manager reuses your nixpkgs pin instead of its own nested lock entry as a separate copy. That reduces “two nixpkgs versions in one eval” confusion and often saves eval work.
Anatomy of a lock node (conceptual)
"nixpkgs": {
"locked": {
"lastModified": 1700000000,
"narHash": "sha256-…",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "abcdef…",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
}original is policy (track nixos-26.05); locked is the decision (this commit).
Theory 2 — Update operations (precision toolkit)
| Command | Effect |
|---|---|
nix flake lock |
Refresh lock metadata without changing intended refs (often no-op if already locked) |
nix flake update |
Update all inputs to latest matching their url refs |
nix flake update nixpkgs |
Update only the nixpkgs input |
nix flake update home-manager |
Update only that input |
nix flake metadata |
Show resolved pins / last modified |
Modern CLI also supports updating multiple named inputs; check nix flake update --help on your Nix version.
Mental model
flake.nix → policy ("track nixos-26.05")
flake.lock → decision ("this commit of that policy")
Changing flake.nix URLs without updating lock can error or keep stale locks depending on change type—always re-lock intentionally and commit both files together when inputs change.
Theory 3 — How to read a lock diff
git diff flake.lock | head -200Look for:
- Which input changed (
"nixpkgs": {block) - Old
revvs newrev narHashchange (expected when rev changes)- Nested inputs (if not
follows)
Optional deep inspection:
nix flake metadata
nix flake metadata --json | jq '.locks.nodes.nixpkgs'Map rev to a GitHub compare URL when investigating regressions:
https://github.com/NixOS/nixpkgs/compare/<old>...<new>
Nested duplicates
If you see nixpkgs_2 style nodes, a dependency brought its own nixpkgs. Prefer:
inputs.foo.inputs.nixpkgs.follows = "nixpkgs";Then re-lock and confirm the duplicate disappears or shrinks.
Theory 4 — Pin strategies (pick a house rule)
| Strategy | When | Risk |
|---|---|---|
Track release branch (nixos-26.05) + lock commits |
Default for this book | Branch tip moves; you choose when to update |
Pin exact rev in flake.nix |
Freeze hard for a milestone | Manual bumps; easy to forget security updates |
Track nixos-unstable |
Bleeding edge lab | Breakage rate higher |
| Multiple flakes (host vs tools) | Advanced separation | Operational complexity |
Recommended for lab hosts: track nixos-26.05 (and matching HM release), commit lock, update one input per change window, rebuild + smoke-test before updating the next.
Security vs stability
Updating nixpkgs is how you receive package and module fixes. Never freeze a public-facing host forever “because it works.” Discipline is when and how, not whether.
| Cadence idea | Fit |
|---|---|
| Weekly selective nixpkgs on lab | Learning hosts |
| PR-based lock bumps in CI | Teams |
| Point release freezes for demos | Talks / workshops |
Unattended full flake update nightly |
Usually a bad default for hosts |
Theory 5 — Registries (light literacy)
Flake registries map short names (nixpkgs) to URLs for ad-hoc commands like nix run nixpkgs#hello.
| Concern | Practice |
|---|---|
| Global registry | Convenient for exploration |
| Your host flake | Should use explicit inputs, not hope the registry matches |
| CI | Prefer locked flake in repo over registry luck |
nix registry listIf global nixpkgs points at unstable while your flake pins 26.05, that is fine for one-off nix run—as long as you never assume those are the same pin.
Registry vs input (summary)
nix run nixpkgs#hello → registry resolution (may float)
nix build .#packages… → your flake.lock
nixos-rebuild --flake .# → your flake.lock
Theory 6 — Multi-machine and “reproducible”
Same lock + same system (x86_64-linux) → same evaluation of inputs. Realization may still:
- Download substitutes vs build locally
- Differ if impure eval sneaks in (
builtins.currentSystemmisuse, unpinnedfetchTarball, IFD surprises) - Differ across architectures
Lock discipline is necessary, not always sufficient, for bit-identical everything—but it is the main lever you control daily.
Clone checklist
git clone <repo>
cd <repo>
git status # flake.lock must be present
nix flake metadata
sudo nixos-rebuild switch --flake .#labMissing lock → everyone invents their own “26.05 tip.”
Theory 7 — Commit policy for locks
| Do | Don’t |
|---|---|
Commit flake.lock with the change that needs it |
Gitignore lock for NixOS host flakes |
| One logical update per commit when possible | Mix lock bumps with huge unrelated refactors |
| Note why you updated in commit body | “chore: update” with no smoke test |
Keep flake.nix + flake.lock paired in reverts |
Restore only one of the pair |
Example commit body:
flake: update nixpkgs (26.05 tip)
Smoke: sshd active, rebuild switch OK, hello from devShell OK.
Revert pattern
git checkout HEAD~1 -- flake.lock
# if flake.nix inputs also changed, restore that too:
# git checkout HEAD~1 -- flake.nix flake.lock
sudo nixos-rebuild switch --flake .#labTheory 8 — Inputs that should co-move
| Pair | Rule of thumb |
|---|---|
nixpkgs nixos-26.05 + HM release-26.05 |
Keep major/minor aligned |
| nixpkgs + sops-nix | follows nixpkgs; update sops after nixpkgs if needed |
| flake-parts + nixpkgs | Framework usually tracks independently; still follows when offered |
Updating HM without nixpkgs (or the reverse) is a common source of obscure option errors. Prefer: update nixpkgs → rebuild → update HM → rebuild.
Worked example — Selective update narrative
Session A: update nixpkgs only → rebuild lab → OK
Session B: add home-manager input → lock appears → rebuild
Session C: update home-manager only → HM activation issue → revert lock hunk
Reverting a bad lock is often faster than debugging three simultaneous input moves.
Worked commands
cp flake.lock /tmp/flake.lock.bak
nix flake update nixpkgs
git diff --stat flake.lock
nixos-rebuild build --flake .#lab
# if OK:
sudo nixos-rebuild switch --flake .#lab
# if bad:
cp /tmp/flake.lock.bak flake.lock
sudo nixos-rebuild switch --flake .#labExercises
Exercise 1 — Snapshot current pins
cd /path/to/your-flake
nix flake metadata
cp flake.lock /tmp/flake.lock.bak
git statusJournal:
- List every input name and its locked
rev(short hash is fine) - Confirm
flake.lockis tracked by git - Note whether any input uses
follows
Exercise 2 — Read the lock structure
nix flake metadata
nix flake metadata --json | jq '.locks.nodes | keys' 2>/dev/null || trueOpen flake.lock and identify:
- The
rootnode’s inputs - The
nixpkgsnode’srevandnarHash - Any nested
nixpkgs_2style duplicates (candidate forfollows)
Exercise 3 — Selective update of one input
Only if you accept rebuild churn. On a disposable lab host:
nix flake update nixpkgs
git diff --stat flake.lock
git diff flake.lock | head -120Build before switch:
nixos-rebuild build --flake .#lab
sudo nixos-rebuild switch --flake .#labSmoke checklist:
systemctl is-system-running || true
systemctl is-active sshd
nix --versionIf break: restore backup lock and re-switch.
Exercise 4 — Prove “update all” is louder
On a throwaway branch:
git checkout -b experiment/flake-update-all
nix flake update
git diff --stat flake.lockCount how many inputs / revs moved. Then restore:
git checkout main # or your real branch name
git branch -D experiment/flake-update-allWrite one sentence: why selective update is the default for hosts.
Exercise 5 — Document house rules
Create docs/lock-policy.md:
# Lock policy (lab)
- Track nixpkgs nixos-26.05
- Commit flake.lock always
- Update one input per change window
- Smoke: rebuild + SSH + one app path
- Revert lock on failure before deep debug
- Align HM release-* with nixos-*Exercise 6 — Registry awareness (read-only)
nix registry list
nix flake metadataNote if the registry’s nixpkgs rev differs from the flake’s. No need to change registries.
Exercise 7 — follows fix (if needed)
If you have (or add) a second input with its own nixpkgs:
inputs.foo.inputs.nixpkgs.follows = "nixpkgs";nix flake lock
nix flake metadata --json | jq '.locks.nodes | keys'Exercise 8 — Compare URL construction
Write the GitHub compare URL for your last nixpkgs bump (from Exercise 3) or a synthetic old→new pair from metadata history. Bookmark the pattern.
Exercise 9 — Impurity awareness list
List three things that can still differ with the same lock (substituters, arch, impure builtins). One sentence each.
Exercise 10 — CI anti-pattern note
In docs/lock-policy.md, add: “CI must not run nix flake update on every job; lock bumps are deliberate PRs.”
Exercise 11 — Paired restore drill
# simulate bad partial restore understanding
git show HEAD:flake.lock >/dev/null
# document: always restore flake.nix + flake.lock together when input URLs changeExercise 12 — Multi-input update plan
Write the order you will use when adding Home Manager (next chapters): lock HM → follows → rebuild → only then update nixpkgs again.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Everyone’s host differs after clone | Missing or uncommitted flake.lock |
follows not set → two nixpkgs |
Add inputs.foo.inputs.nixpkgs.follows = "nixpkgs" |
| Updated lock, forgot rebuild | switch / boot to activate |
CI uses nix flake update every run |
Pin lock in repo; update in deliberate PRs |
| “I’m on 26.05” but lock is months old | Expected until you update; schedule updates |
Restored flake.nix but not lock |
Always restore the pair when debugging inputs |
| Private input auth fails after update | Token/SSH access issue, not lock format |
| HM release ≠ nixpkgs release | Align release-26.05 with nixos-26.05 |
Reviewed only flake.nix in PR |
Always review lock diff too |
| Massive lock + feature PR | Split: lock bump PR vs feature PR |
Checkpoint
- Can explain what
rev+narHashbuy you - Can update one named input and read the lock diff
- Have a written house lock policy
- Know how to revert
flake.lockquickly - Understand registry ≠ flake pin
followsplanned or applied for companion inputs- Know why full
nix flake updateis loud on hosts - Commit policy treats lock as first-class
Journal (optional)
Write three personal gotchas before continuing—especially any surprise nested nixpkgs or registry mismatch.