org-howto/articles/2026/06/nix-binary-cache.org
Roland Conybeare 40fbc6eaaa
All checks were successful
Deploy / publish (push) Successful in 21s
+ nix binary cache article
2026-06-21 17:57:36 -04:00

187 lines
6.2 KiB
Org Mode

#+title: [20jun2026] nix binary cache
# ----------------------------------------------------------------
# +targs: @nixpkgs @nix @nxfs @binary-cache @FOD
# ----------------------------------------------------------------
#+description: nix binary cache
#
# org-publish options
#
# ^:{} require a_{b} before assuming that b should be subscripted.
# without this option a_b will automatically subscript b.
#+options: ^:{}
#
# emacs-specific options
#+startup: showall
#
# html exporter options
#+language: en
#+keywords: fontconfig nixpkgs nix-env
#+setupfile: ../../../ext/fniessen/theme-readtheorg.setup
#
#+html_head: <link rel="shortcut icon" type="image/x-icon" href="/web/img/favicon.ico" />
#+html_link_home: ../../../index.html
#
# not using: prefer theme-readtheorg
# +infojs_opt: view:showall mouse:#ffc0c0 toc:nil ltoc:nil path:/web/ext/orginfo/org-info.js
# +html_head: <link rel="stylesheet" type="text/css" href="/web/css/primary.css" />
* Problem
I run nix with a non-standard nix store location (see https://github.com/Rconybea/nix-from-scratch).
This means that all packages build from source, and the default substituter =cache.nixos.org=
is not usable.
One problem that arises is that internet endpoints aren't always permanent;
over time URLs change or disappear.
Nix' solution for this is a *binary cache*.
This note describes setting one up.
Typical purpose is to record nix derivation *outputs* separately from the nix store.
Although we'll get that as a byproduct, our goal is actuall to store derivation *inputs*,
i.e. so-called fixed-output *source derivations*.
* Setup
** Create Cache Directory and Key
I have a network attached storage device with multiple TB of unused capacity.
We'll choose a location for holding nix store artifacts. As of June 2026,
my nix store contains about 190GB.
We will make the binary cache available at =/mnt/nas1/share2/nix-cache=.
#+begin_src sh
nix-store --generate-binary-cache-key nas1-=roland-1 \
~/.config/nix/nas1-cache.secret \
~/.config/nix/nas1-cache.public
mkdir /mnt/nas1/share2/nix-cache
cp ~/.config/nix/nas1-cache.public /mnt/nas1/share2/nix-cache/
#+end_src
** Copy Nix Store
#+begin_src sh
nix copy --all \
--to 'file:///mnt/nas1/share2/nix-cache?secret-key=/path/to/.config/nix/nas1-cache.secret&compression=zstd'
#+end_src
** Register Store as Substituter
in =~/.confrig/ni9x/nix.confg=:
#+begin_src conf
substituters = file:///mnt/nas1/share2/nix-cache
trusted-public-keys = nas1-roland-1:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
#+end_src
* Maintenance
Added some scripts to =nix-from-scratch=:
Manual script
#+begin_example
# nix-from-scratch/binary-cache/nix-cache-sync.sh
#!/usr/bin/env bash
#
# nix-cache-sync.sh — push the local nix store to the NAS binary cache.
#
# This workstation runs a from-source nix with a relocated store
# (/home/roland/nixroot/nix/store), so it can't use cache.nixos.org and builds
# everything from source. The NAS binary cache is our durable, self-controlled
# archive of sources + build outputs, and the only configured substituter
# (see ~/.config/nix/nix.conf).
#
# This copies every currently-valid store path into the cache. It is incremental
# (paths already present are skipped), so it's cheap to re-run — use it as the
# after-a-build top-up, or let the systemd timer (../systemd/) run it nightly.
#
# Usage:
# nix-cache-sync.sh # sweep the whole store (--all)
# nix-cache-sync.sh PATH... # copy only the given store paths (+ closures)
#
# Override defaults via env:
# NIX_CACHE_MOUNT NAS mount point that must be mounted (default /mnt/nas1/share2)
# NIX_CACHE_DIR cache directory (default $NIX_CACHE_MOUNT/nix-cache)
# NIX_CACHE_SECRET signing key (default ~/.config/nix/nas1-cache.secret)
# NIX_CACHE_COMPRESSION zstd|xz|none (default zstd)
# NIX nix binary (default /home/roland/nixroot/bin/nix)
set -euo pipefail
MOUNT="${NIX_CACHE_MOUNT:-/mnt/nas1/share2}"
CACHE_DIR="${NIX_CACHE_DIR:-${MOUNT}/nix-cache}"
SECRET="${NIX_CACHE_SECRET:-${HOME}/.config/nix/nas1-cache.secret}"
COMPRESSION="${NIX_CACHE_COMPRESSION:-zstd}"
NIX="${NIX:-/home/roland/nixroot/bin/nix}"
die() { echo "nix-cache-sync: $*" >&2; exit 1; }
[ -x "$NIX" ] || die "nix binary not found/executable: $NIX"
mountpoint -q "$MOUNT" || die "NAS not mounted at $MOUNT — refusing to sync"
[ -f "$SECRET" ] || die "signing key not found: $SECRET"
mkdir -p "$CACHE_DIR"
to="file://${CACHE_DIR}?secret-key=${SECRET}&compression=${COMPRESSION}"
if [ "$#" -gt 0 ]; then
echo "nix-cache-sync: copying ${#} path(s) -> ${CACHE_DIR}"
exec "$NIX" copy --to "$to" "$@"
else
echo "nix-cache-sync: sweeping whole store -> ${CACHE_DIR} (incremental)"
exec "$NIX" copy --all --to "$to"
fi
#+end_example
Systemd service to refresh cache
#+begin_example
# nix-from-scratch/systemd/nix-cache-sync.service
# Sync the local (relocated) nix store to the NAS binary cache.
#
# Reference unit — NOT auto-installed. See README.md in this directory for
# manual install. Companion: nix-cache-sync.timer.
#
# Captures every currently-valid store path (sources + build outputs) into the
# NAS cache. Incremental: paths already in the cache are skipped, so it is cheap
# to run repeatedly.
[Unit]
Description=Sync nix store to NAS binary cache (source + build-output archive)
[Service]
Type=oneshot
# Skip (not fail) the run if the NAS is not mounted.
ExecCondition=/usr/bin/mountpoint -q /mnt/nas1/share2
ExecStart=/home/roland/nixroot/bin/nix copy --all --to "file:///mnt/nas1/share2/nix-cache?secret-key=%h/.config/nix/nas1-cache.secret&compression=zstd"
# Be gentle: this competes with interactive builds for CPU and NFS I/O.
Nice=10
IOSchedulingClass=idle
#+end_example
Plus nightly trigger
#+begin_example
# nix-from-scratch/systemd/nix-cache-sync.timer
# Nightly trigger for nix-cache-sync.service.
#
# Reference unit — NOT auto-installed. See README.md for manual install.
[Unit]
Description=Nightly sync of nix store to NAS binary cache
[Timer]
OnCalendar=daily
# If the machine was off at the scheduled time, run once after next boot.
Persistent=true
# Avoid a thundering-herd exactly at midnight.
RandomizedDelaySec=15min
[Install]
WantedBy=timers.target
#+end_example