6.7 KiB
petersweb-infra/nixos — CLAUDE.md
What this repo is
NixOS configuration for a single Hetzner server ("mainframe") running Philip Peterson's personal/Quine Foundation infrastructure. One machine, one flake configuration: nixosConfigurations.mainframe.
Applying changes
./apply.sh # git pull + nixos-rebuild switch --flake .#mainframe
# or manually:
nixos-rebuild switch --flake /root/petersweb-infra/nixos#mainframe
File layout
| Path | Purpose |
|---|---|
flake.nix |
Single flake, defines nixosConfigurations.mainframe |
hetzner.nix |
Hardware config: GRUB on /dev/sda, static networking, openssh |
linux.nix |
Main system config: services, secrets, docker containers, ACME certs |
nginx.nix |
Nginx virtual hosts and reverse proxies |
firewall.nix |
Open TCP ports |
disk-config.nix |
disko disk layout |
cloned_repos/ |
pullomatic configs for auto-pulling git repos to /etc/pullomatic/ |
arion/ |
Arion (docker-compose-like) for Forgejo |
arion-riverside/ |
Arion for the Riverside service |
pullomatic/ |
Rust tool that watches git remotes and pulls on a schedule |
invoke-ddns/ |
Python DDNS updater for NearlyFreeSpeech DNS |
secrets/ |
agenix-encrypted secrets |
keys/ |
SSH public keys used as age recipients |
system/ |
User definitions and home-manager config |
pdxdestiny/ |
Static site files for pdxdestiny.com |
vnc-desktop/ |
Dockerfile + build scripts for the KDE Plasma VNC desktop container |
Secrets (agenix)
Secrets live in secrets/*.age. They are encrypted with the key in keys/mainframe.pub (which is identical to /root/.ssh/id_rsa_nix.pub on the server).
Important: Agenix uses three identity paths for decryption (see activation script):
/etc/ssh/ssh_host_rsa_key/etc/ssh/ssh_host_ed25519_key/root/.ssh/id_rsa_nix← this is the actual working key
The decrypted secrets land at /run/agenix/<name> at boot.
Secret format matters
The NixOS gitea-actions-runner module reads the token via EnvironmentFile=, so the secret file must be in KEY=VALUE format:
forgejo-runner-token.age→ must containTOKEN=<raw_token>(not just the raw token)nearlyfreespeech.age→ containsNEARLYFREESPEECH_API_KEY=...andNEARLYFREESPEECH_LOGIN=...webdav.age→ containsWEBDAV_PASSWORD=...anthropic-api-key.age→ containsANTHROPIC_API_KEY=...postmark.age→ containsPOSTMARK_SERVER_TOKEN=...
Re-encrypting a secret
# Encrypt new content for the mainframe key
printf "TOKEN=newvalue\n" | nix run nixpkgs#age -- \
-r "$(cat /root/petersweb-infra/nixos/keys/mainframe.pub)" \
-o /root/petersweb-infra/nixos/secrets/forgejo-runner-token.age
# Verify it decrypts correctly
nix run nixpkgs#age -- -d -i /root/.ssh/id_rsa_nix \
/root/petersweb-infra/nixos/secrets/forgejo-runner-token.age
Note: secrets/default.nix is the agenix recipients file. Agenix looks for secrets.nix by default — to use the CLI with this repo's default.nix, you'd need a symlink or pass the path manually. Use age directly instead (as above).
Key services
| Service | Description |
|---|---|
gitea-runner-ubuntu.service |
Forgejo (Gitea) Actions CI runner, uses docker images |
forgejo-arion.service |
Forgejo itself, run via Arion/Podman |
riverside-arion.service |
Riverside app, run via Arion/Docker |
podman-navidrome.service |
Navidrome music server on port 4533 |
podman-nextcloud.service |
Nextcloud/SSH container on port 8087 |
podman-sync.io.service |
sync.io app on port 9090 |
podman-blog-quine.service |
Blog on port 3010 |
podman-coldairnetworks.service |
Cold Air Networks site on port 3012 |
podman-vnc-desktop.service |
KDE Plasma desktop, noVNC on port 6080 (localhost only) |
build-vnc-image.service |
Builds the VNC desktop image from vnc-desktop/; runs before podman-vnc-desktop |
| nginx | Reverse proxy + ACME certs for multiple domains |
Virtualisation
- Podman is used for all OCI containers (
virtualisation.oci-containers.backend = "podman") — navidrome, nextcloud, blog, VNC desktop, etc. — and for Forgejo via Arion. - Docker is still present for the Riverside Arion stack.
DOCKER_HOSTfor the gitea-runner is set tounix:///run/podman/podman.sock.- The gitea-runner runs docker images for CI jobs, so the
gitea-runneruser is in thedockerandpodmansupplementary groups.
VNC desktop
podman-vnc-desktop.service runs a KDE Plasma desktop inside a container, accessible via noVNC at localhost:6080 (reverse-proxied by nginx). The image is built locally — no registry involved.
- Image source:
vnc-desktop/Dockerfile(Ubuntu 24.04, TigerVNC, KDE, Firefox, patched Discover) - Auto-rebuild:
build-vnc-image.serviceruns on boot and onnixos-rebuild switchwhenevervnc-desktop/changes. The trigger isvncContext = builtins.path { path = ./vnc-desktop; }— a Nix store path that invalidates when any file in the directory changes. - Auto-restart:
podman-vnc-desktop.servicehasrestartTriggers = [ vncContext ], so the container restarts automatically after a rebuild duringnixos-rebuild switch. - Secrets:
VNC_PASSWORDandROOT_PASSWORDcome fromage.secrets.vnc-password. - Discover logging:
vnc-desktop/discover-logging/contains a build-time patch (patch.py) that instrumentsPKTransaction.cppwithqWarningcalls to diagnose hanging installs. Logs visible viapodman logs vnc-desktop.
Networking / DNS
- Dynamic DNS via
invoke-ddns(NearlyFreeSpeech provider). - ACME certs issued via DNS challenge for
philippeterson.comandwebdav.philippeterson.com. - Forgejo accessible on ports 3000 (HTTP) and 2200 (SSH).
Known gotchas
gitea-runneris aDynamicUserin the systemd service, so it has no persistent uid. Settingage.secrets.forgejo-runner-token.owner = "gitea-runner"causes a chown error at activation; useowner = "root"instead (the service reads it viaEnvironmentFilewhich runs as root before privilege drop).secrets/default.nixmust have the public key fromkeys/mainframe.pubas the recipient — if the host SSH keys change, you must also updatemainframe.puband re-key all secrets.pullomaticuses/root/.ssh/id_rsa.pem(a PEM-format SSH key) to pull private git repos.- ACME cyclic dependency list:
linux.nixhas asystemd.services.nginx.after = lib.mkForce [...]list that breaks a systemd cycle between nginx and ACME services. Every new domain added withenableACME = trueinnginx.nixmust also have itsacme-selfsigned-<domain>.serviceadded to this list inlinux.nix, otherwise nixos-rebuild will fail with a cyclic dependency error.