Files
homelab/CLAUDE.md
T
darmanandClaude Sonnet 5 d7a66f3e3b Reorganize services/ into category subfolders
Group service modules by category (media, network, vpn, identity,
dev, desktop) to make the growing services/ dir easier to navigate.
containers.nix stays at the top level since it's a shared backend,
not a single-category service.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 06:08:43 +02:00

5.3 KiB
Raw Blame History

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), neptun (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/<cat>/*.nix   # one reusable NixOS module per service, grouped by category
                       # (media, network, vpn, identity, dev, desktop); each opens
                       # ITS OWN firewall ports. services/containers.nix (podman
                       # backend) stays at the top level, shared across categories.
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. services/vpn/tailscale.nix, services/network/caddy.nix used by jupiter and neptun).

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>.yaml is 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/neptun decrypt with their ssh host key (ssh-to-age recipient), shipped at install via nixos-anywhere --extra-files.
  • mercury (SD image, no --extra-files) uses a dedicated age key at /var/lib/sops-nix/age.txt./scripts/deploy flash writes it to the ext4 root partition.
  • A service password that must come from sops but whose module has no passwordFile hook (pihole, adguard) is injected via sops.templates → an env file → the service (FTLCONF_* for pihole). See hosts/mercury/configuration.nix.

Non-obvious gotchas (all learned the hard way)

  • aarch64 (mercury): the x86 laptop needs extra-platforms = aarch64-linux in /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/network/pihole.nix, official image, host networking, caps NET_ADMIN/NET_RAW/SYS_NICE/CHOWN, FTLCONF_* env config). The native services.pihole-ftl module segfaults on the Pi 3B+ aarch64 — do not switch back.
  • services.unbound.resolveLocalQueries = false is 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 upstream networking.nameservers; pihole forwards to unbound explicitly at 127.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 in configuration.nix (generate-config misses them); the 16TB×2 RAID0 data lives on /mnt/data with nofail, kept OUT of disko (never wiped).
  • disko wipes only the OS disk named in hosts/<h>/disk-config.nix; data disks are plain fileSystems in configuration.nix.
  • nixos-anywhere/kexec needs a writable root; ZimaOS root is read-only, hence the ./scripts/deploy kexec step that streams a RAM installer (with static cpio/gzip since ZimaOS lacks them).