5.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Flake-based NixOS config for a homelab. Hosts: jupiter (ZimaBlade NAS, x86_64),
vps (netcup public reverse proxy + tailnet node, x86_64), mercury (Raspberry
Pi 3B+ DNS/DHCP, aarch64). See README.md for the full install/deploy walkthrough.
Layout
flake.nix # nixosConfigurations: real hosts + test/util targets
common.nix # shared base: user darman (key-only ssh), nix settings, firewall :22, tz
services/*.nix # one reusable NixOS module per service; each opens ITS OWN firewall ports
hosts/<h>/ # configuration.nix + disk-config.nix (disko) + hardware-configuration.nix + secrets.nix
secrets/<h>.yaml # sops-nix, age-encrypted per host
scripts/deploy # config-agnostic deploy wrapper (all args mandatory)
scripts/edit_secrets
.sops.yaml # per-host encryption rules (admin key + each host's key)
A host = common.nix + the services/* modules it imports + its hosts/<h>/configuration.nix.
services/ modules are engine-agnostic and shared across hosts (e.g. tailscale.nix,
caddy.nix used by jupiter and vps).
Commands
Eval/verify a config before deploying (eval only checks the module tree, not freeform config like pihole's TOML or a container's runtime):
nix eval --raw .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath
Deploy (from a non-NixOS laptop too — runs nixos-rebuild/nixos-anywhere via nix run):
./scripts/deploy switch <config> <host> # daily rebuild + activate
./scripts/deploy install <config> <host> # first install (nixos-anywhere, wipes OS disk)
./scripts/deploy kexec <host> # RO-root box (ZimaOS): kexec into a RAM installer first
./scripts/deploy image mercury # build the aarch64 SD image
./scripts/deploy flash mercury /dev/sdX # build + write SD + drop the sops age key
Secrets (needs the admin age key at ~/.config/sops/age/keys.txt):
./scripts/edit_secrets secrets/<host>.yaml
Test a service config BEFORE touching hardware — always do this for nontrivial changes:
# x86 QEMU VM of mercury's DNS/DHCP stack (fast; validates pihole/unbound at runtime)
nix build .#nixosConfigurations.mercury-vm.config.system.build.vm -o result
./result/bin/run-mercury-vm-vm # ssh -p 2223 darman@localhost (pw: test)
# jupiter services as a VirtualBox OVA
nix build .#nixosConfigurations.jupiter-vbox.config.system.build.virtualBoxOVA
Secrets (sops-nix)
- Each
secrets/<host>.yamlis encrypted to the admin key (edit) + that host's key (runtime decrypt); rules in.sops.yaml. Private keys live OFF-repo:~/.config/sops/age/keys.txt(admin),~/.config/homelab/<host>/(host keys). - jupiter/vps decrypt with their ssh host key (
ssh-to-agerecipient), shipped at install vianixos-anywhere --extra-files. - mercury (SD image, no
--extra-files) uses a dedicated age key at/var/lib/sops-nix/age.txt—./scripts/deploy flashwrites it to the ext4 root partition. - A service password that must come from sops but whose module has no
passwordFilehook (pihole, adguard) is injected viasops.templates→ an env file → the service (FTLCONF_*for pihole). Seehosts/mercury/configuration.nix.
Non-obvious gotchas (all learned the hard way)
- aarch64 (mercury): the x86 laptop needs
extra-platforms = aarch64-linuxin/etc/nix/nix.custom.conf(NOT/etc/nix/nix.conf— Determinate Nix regenerates that)qemu-user-static-binfmt, else emulated builds fail with "platform mismatch". Or build on the Pi with--build-host darman@<ip>.
- pihole on mercury is a CONTAINER (
services/pihole.nix, official image, host networking, caps NET_ADMIN/NET_RAW/SYS_NICE/CHOWN,FTLCONF_*env config). The nativeservices.pihole-ftlmodule segfaults on the Pi 3B+ aarch64 — do not switch back. services.unbound.resolveLocalQueries = falseis required: unbound listens on :5335, so leaving it true points the host's resolv.conf at 127.0.0.1:53 with nothing there → boot-time DNS deadlock (starves image pulls / list downloads). Host resolves via upstreamnetworking.nameservers; pihole forwards to unbound explicitly at127.0.0.1#5335.- Remote deploy pushes unsigned closures: hosts set
nix.settings.trusted-users = [ "root" "@wheel" ](in common.nix) so a laptop-built closure is accepted by the target. - jupiter:
boot.kernelParams = [ "reboot=pci" ](warm reboot hangs on that board); eMMC initrd modules pinned inconfiguration.nix(generate-config misses them); the 16TB×2 RAID0 data lives on/mnt/datawithnofail, kept OUT of disko (never wiped). - disko wipes only the OS disk named in
hosts/<h>/disk-config.nix; data disks are plainfileSystemsinconfiguration.nix. nixos-anywhere/kexec needs a writable root; ZimaOS root is read-only, hence the./scripts/deploy kexecstep that streams a RAM installer (with static cpio/gzip since ZimaOS lacks them).