GC and store hygiene
GC and store hygiene
Goal: Measure store size, explain GC roots, set a retention policy for generations and CI, and run optimise/GC safely without deleting live systems.
Why this day exists
Deploy + CI + packaging fill disks silently. Out-of-disk mid-activate is a self-inflicted outage. Hygiene is an operational skill equal to deploy tooling—Capstone hosts die from full /nix as often as from bad modules.
Theory 1 — What GC deletes
Garbage collection deletes store paths that are not reachable from any GC root.
| Root examples | Keeps alive |
|---|---|
/run/current-system |
Running NixOS closure |
/nix/var/nix/profiles/* |
System generations, user profiles |
~/.local/state/nix/profiles |
Modern user profiles |
/nix/var/nix/gcroots/* |
Explicit pins |
| Result symlinks in cwd | ./result from nix build |
| Indirect roots / registries | Various tooling pins |
nix-store --gc --print-roots | head -n 50
nix-collect-garbage --dry-run
nix store gc --dry-run # modern CLI shape; flags evolve—check --helpMental model
GC root → profile/generation/result link → closure of store paths
Anything not reachable is eligible for deletion
Deleting a project directory does not free the store if roots still point at builds.
Theory 2 — Generations vs store GC
# List system generations
sudo nix-env -p /nix/var/nix/profiles/system --list-generations
# or inspect profile links
ls -l /nix/var/nix/profiles/system*
# Delete old generations then GC
sudo nix-collect-garbage --delete-older-than 14d
# aggressive: remove all old gens of profiles
# sudo nix-collect-garbage -d| Command | Effect |
|---|---|
nix-collect-garbage |
GC unreachable paths |
--delete-older-than |
Drop old gens then GC |
-d |
Aggressive old gen removal |
nix store gc |
Modern CLI GC |
nix store optimise |
Hardlink identical files |
Deleting generations removes rollback options. Keep enough to survive a bad deploy week—especially before Capstone freeze and DR drills.
User vs system profiles
Home-Manager and user nix profile installs have their own generation chains. System GC policy does not automatically equal user hygiene.
nix profile list 2>/dev/null || true
ls ~/.local/state/nix/profiles 2>/dev/null || trueTheory 3 — Policy template
| Host class | Generation retention | Notes |
|---|---|---|
| Laptop | 7–14 days | Disk pressure |
| Prod VM | 14–30 days + keep last N | Rollback window |
| CI runner | Daily GC; few profiles | Ephemeral preferred |
| Builder | Weekly GC; keep hot cache set | Balance disk vs rebuild |
| Capstone lab | ≥14 days through the rollback and tests chapter | Rollback drills need gens |
# NixOS automatic GC example
{ ... }:
{
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 14d";
};
nix.optimise.automatic = true;
# proactive free-space GC thresholds (bytes)
nix.settings.min-free = 10 * 1024 * 1024 * 1024; # 10 GiB
nix.settings.max-free = 50 * 1024 * 1024 * 1024;
}min-free triggers proactive GC when disk is low—excellent on small roots and cloud boxes.
Theory 4 — Finding what holds space
df -h /nix
nix path-info -Sh /run/current-system
nix path-info -rS /run/current-system | sort -k2 -n | tail -n 20
# Why is path alive?
nix-store --query --roots /nix/store/…-something# Old result links (common disk vampires)
find "$HOME" -maxdepth 3 -type l \( -name result -o -name 'result-*' \) 2>/dev/null# Largest store paths (approx; tools vary)
nix path-info -rS /run/current-system | sort -k2 -h | tailCI-specific roots
Runners accumulate result links and old profiles if jobs are not ephemeral. Prefer clean workspaces per job; document GC for self-hosted runners in docs/CI.md.
Theory 5 — optimise (hardlinks)
nix store optimise
# or
sudo nix-store --optimiseIdentical files across store paths become hardlinks → often saves GBs when many similar closures exist (multiple generations, overlapping devshells). Safe; CPU-heavy first run.
Backup tools that do not understand hardlinks may inflate sizes or behave oddly. Know your backup tool’s hardlink policy (the backups chapter).
Theory 6 — Incident patterns
| Symptom | First checks |
|---|---|
No space left on device during build |
df -h /nix /; roots; old gens; result links |
| Activate failed mid-switch | Free space; incomplete profile; repair with boot generation |
| GC “deleted my package” | Missing root; rebuild; avoid bare -d in prod |
| Rollback missing | Generations deleted by policy |
| Disk full only on CI | Self-hosted runner hygiene |
Never run destructive GC on prod without:
- Confirming current-system root
- Confirming retention window
- Preferring
--delete-older-thanover blind-d
Worked example — hygiene report script
#!/usr/bin/env bash
# scripts/store-report.sh
set -euo pipefail
echo "== disk =="
df -h /nix 2>/dev/null || df -h /
echo "== current system closure =="
nix path-info -Sh /run/current-system 2>/dev/null || true
echo "== gc roots (sample) =="
nix-store --gc --print-roots 2>/dev/null | head -n 40 || true
echo "== result symlinks =="
find "$HOME" -maxdepth 2 -type l -name 'result*' 2>/dev/null || true
echo "== system generations =="
sudo nix-env -p /nix/var/nix/profiles/system --list-generations 2>/dev/null | tail -n 15 || truechmod +x scripts/store-report.sh
./scripts/store-report.sh | tee ~/lab/90daysofx/02-nixos/day67/before.txtLab — multi-step
Suggested notes: ~/lab/90daysofx/02-nixos/day67
Step 1 — Baseline metrics
Record df -h /nix (or /) and current-system size via nix path-info -Sh.
Step 2 — Map roots
Print roots; identify unexpected result links and old profiles. Screenshot or tee a sample.
Step 3 — Safe cleanup
Remove stale ./result from lab dirs; GC without aggressive -d first:
nix store gc
df -h /nixStep 4 — Generation policy
List generations; delete only those outside your written policy window.
sudo nix-collect-garbage --delete-older-than 14dStep 5 — Enable automatic GC on lab host
Commit nix.gc (+ optional min-free / optimise) settings; deploy or switch.
Step 6 — optimise
Run optimise; record space reclaimed if measurable.
df -h /nix | tee ~/lab/90daysofx/02-nixos/day67/before-optimise.txt
nix store optimise
df -h /nix | tee ~/lab/90daysofx/02-nixos/day67/after-optimise.txtStep 7 — CI runner note
Add GC step for self-hosted runners or rely on ephemeral runners; document in docs/CI.md.
Step 8 — Policy doc
Write docs/STORE-HYGIENE.md (or section in RUNBOOK):
| Host | Retention | Auto GC | Notes |
|---|---|---|---|
| lab | 14d | weekly | |
| laptop | 7d | weekly |
Step 9 — Rollback still possible?
Confirm at least one previous generation remains on lab after cleanup. If not, you over-pruned—adjust policy.
Step 10 — Profile audit (user + HM)
# User profiles / modern nix profile
nix profile list 2>/dev/null | tee ~/lab/90daysofx/02-nixos/day67/user-profile.txt || true
# Home-Manager generations if present
ls -la /nix/var/nix/profiles/per-user/"$USER"/ 2>/dev/null || true
ls -la ~/.local/state/home-manager 2>/dev/null || trueWrite one sentence: how user-profile retention differs from system nix.gc on this host.
Step 11 — Dry-run before aggressive delete
# Prefer dry-run / list before destroy
nix-collect-garbage --delete-older-than 30d --dry-run 2>&1 \
| tee ~/lab/90daysofx/02-nixos/day67/gc-dry-run.txt || trueIf dry-run is unavailable on your Nix version, list generations explicitly and delete only named old gens.
Worked example — min-free incident story
Disk at 98% on small cloud root:
1) df -h /nix
2) remove stale result links in ~ and CI workspaces
3) nix-collect-garbage --delete-older-than 7d # laptop policy, not prod
4) enable nix.settings.min-free so next time GC starts earlier
5) consider separate /nix partition next reprovision (Disko)
Record whether you hit this during Stage V/VI. Capstone hosts on 20 GiB disks fail predictably without policy.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| GC “does nothing” | Roots still hold paths |
| Deleted all rollbacks | Stop bare -d in prod without policy |
/nix on small partition |
Separate partition; min-free |
| optimise confuses backups | Hardlinks; tool configuration |
| Multi-user permissions | GC as root for system profiles |
| HM gens forgotten | User profile hygiene |
| Full disk mid-CI | Ephemeral runners; job GC; slimmer checks |
Checkpoint
- Measured store before/after
- Explained roots with real examples from your machine
- Retention policy written and applied on lab
- optimise run once
- Auto GC or min-free configured
- At least one rollback generation still present
- User/HM profile note written
docs/STORE-HYGIENE.md(or RUNBOOK section) committed
- Three personal gotchas
Commit
git add .
git commit -m "day67: GC policy and store hygiene"Write three personal gotchas before continuing.