core
Edit on GitHubModule: modules/nixos/core/core.nix
Nix and flakes, the ceilings on disk growth, nix-ld and the locale. Everything here is about keeping something from growing without bound.
Store dedup is SCHEDULED, not automatic
nix.optimise with a date, and not auto-optimise-store. That one runs the hardlinking on EVERY
build, and on btrfs the metadata churn is CoW, which gets expensive on a machine that rebuilds all
day. Scheduled, the work leaves the critical path and goes into an idle window (03:45, away from
the weekly GC, and from the daily restic until it was retired on 24/09/2026).
It is NOT because of the fear that circulates, "auto-optimise corrupts the store": the NixOS/nix#7273 race was fixed, and the assert that claims otherwise is nix-darwin policy. The reason is only WHEN the work happens.
The GC caps generations by COUNT, so it is nh's
The scheduled GC is programs.nh.clean (weekly, nh clean all --keep 10 --keep-one), and
nix.gc is OFF. Until 03/10/2026 it was the other way around, nix.gc with
--delete-older-than 30d. That policy only knows AGE, and on a machine that rebuilds several times
a day 30 days became 77 system generations, 45 GiB that a manual GC freed at once. Nobody rolls back
further than a handful of generations, so the honest ceiling is a count, and
nix-collect-garbage has no --keep N.
- 10 matches
boot.loader.limine.maxGenerations(limine.nix): every generation kept has a menu entry, and no entry points at a collected one. --keep-one:nh clean allalso collects gcroots, the direnv ones included. Without it every project's devShell would be rebuilt after each run.
Two collectors on the same store is exactly rule 14's "two owners for the same artifact". Neither
fails, and the real retention becomes the INTERSECTION of the two policies, which is to say you
think you have 10 generations and you have whatever the other one left. That is why nix.gc went
OFF in the same commit (the nh module also warns when both are on). The space-reactive GC below is
not a second owner: it only fires on a full disk.
The space-reactive GC, and why 15/50 and not 1/5
If free space drops below min-free during a build, Nix collects until it frees max-free and
the build goes on, which avoids "no space left" in the middle of a big rebuild.
Raised from 1 GiB/5 GiB on 30/07. 1 GiB is TOO LATE to be a safety net: starting to collect only when 1 GiB is left is arriving after the accident, and the build that triggered the collection has probably already failed. Here the partition is SHARED with games and media (measured: 506 GiB across Bottles/Jellyfin/Steam against 58 GiB of store), so the space can disappear outside Nix and Nix needs real slack. A 15 GiB floor gives room for a big rebuild; a 50 GiB target avoids collecting again on the next build.
The names matter: in this Nix (2.34.8) they are min-free/max-free. The rename to
gc-threshold/gc-limit plus auto-gc belongs to a newer version and does NOT exist here
(checked with nix config show).
The journal's ceiling
Without SystemMaxUse, journald uses its default: 10% of the filesystem. On this machine (915 G)
that is ~92 GiB it can occupy LEGITIMATELY, with nothing raising a flag, which is the kind of
growth you only discover with a full disk.
Today it sits at 530 MiB, so 2 GiB is a roomy ceiling that still keeps weeks of history. The
concrete lesson behind it, from 30/07: two timers of mine wrote 2148 lines/DAY before they got
LogLevelMax. SystemMaxFileSize caps each file so the rotation is gradual instead of in 1/8
steps.
Why nh replaced nixos-rebuild in the aliases
nh os switch adds two things nixos-rebuild does not give:
- a progress TREE for the build (nix-output-monitor inside) instead of the wall of
building '/nix/store/…'that does not say what is left; - a package DIFF between the current generation and the new one: what went up a version, what came in, what went out. That is information I once spent a whole day extracting by hand comparing store paths.
--ask is opt-in (checked in --help), so it does NOT become interactive: it stays safe over SSH
and inside a script, which matters on this remote-access machine.
The daemon runs at idle priority
Builds max out the cores compiling, and yielding CPU and disk to the interactive session is what keeps a rebuild from freezing the desktop or the Moonlight stream. With the machine idle the build uses everything normally, because idle only yields when something else wants it.
The FAI Meet cache
The FAI Meetings repository builds FAI Meet from source and publishes the result, signed with
fai-meet-cache-1, at https://meet.sup.fai.ufscar.br/nix-cache (its ADR 0020). Declared here, a
nix run of that flake at the published commit downloads the app instead of compiling it. Only
the cache and its public key come in, not the flake: that repository is private and only answers
through the FAI VPN, so as an input it would break this repo's public CI and a restore from
scratch. The installed app stays the AppImage, see fai-meet.
It is a trusted substituter, not a default one. Until 01/11/2026 it sat in
extra-substituters, so every build asked it for every missing path, and the server only answers
through the FAI VPN: with the VPN down, rebuild died on 15 s narinfo timeouts for paths that
cache.nixos.org had all along. Now nothing queries it unless asked, and fai-meet-latest asks
(--option extra-substituters), which an untrusted user may do only because the URL is listed in
extra-trusted-substituters. That listing, and the key, are why it is still declared system-wide:
an untrusted user cannot add a substituter from a flake's nixConfig.
Locale
The system is en_US.UTF-8 by preference: output and errors in English make debugging easier.
pt_BR.UTF-8 is generated too, and the only consumer is the lockscreen's clock, which reads
LC_TIME for the spelled-out date. That is the pt-BR exception rule 17 names. The timezone and
the TTY keymap follow BR, because those are physical facts.