5.2 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 |
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 |
docker-navidrome.service |
Navidrome music server on port 4533 |
docker-nextcloud.service |
Nextcloud/SSH container on port 8087 |
docker-sync.io.service |
sync.io app on port 9090 |
docker-blog-quine.service |
Blog on port 3010 |
docker-coldairnetworks.service |
Cold Air Networks site on port 3012 |
| nginx | Reverse proxy + ACME certs for multiple domains |
Virtualisation
- Podman is used for Forgejo (via Arion).
DOCKER_HOSTfor the gitea-runner is set tounix:///run/podman/podman.sock. - Docker is used for the OCI containers (navidrome, nextcloud, etc.) and the riverside Arion stack.
- The gitea-runner runs docker images for CI jobs, so the
gitea-runneruser is in thedockerandpodmansupplementary groups.
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.