# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is A flake-based, modular NixOS configuration for a single personal server ("eurovm") running on Hetzner Cloud in Europe. There is no application code here — this repo *is* the infrastructure: editing a `.nix` file and deploying is the equivalent of shipping a change. ## Commands There is no local build/test/lint step in the traditional sense — the workflow is evaluate -> deploy against the live host. Nix itself must be installed locally with flakes enabled (`nix --version`). Check the flake evaluates without deploying: ```bash nix flake check ``` Deploy a change (from an x86_64-linux machine): ```bash nixos-rebuild switch --flake .#eurovm \ --target-host admin@ \ --use-remote-sudo ``` Deploy from a non-x86_64-linux machine (e.g. Apple Silicon) — the server builds its own closure: ```bash nixos-rebuild switch --flake .#eurovm \ --target-host admin@ \ --build-host admin@ \ --use-remote-sudo ``` First-time provisioning of a fresh VM (partitions the disk via disko, installs NixOS): ```bash nix run github:nix-community/nixos-anywhere -- \ --flake .#eurovm \ --build-on remote \ root@ ``` Edit encrypted secrets (re-encrypts on save): ```bash sops secrets/secrets.yaml ``` Add a new sops recipient (e.g. after rotating a key or adding a new machine) and re-encrypt: ```bash sops updatekeys secrets/secrets.yaml ``` Full first-deploy / bootstrap sequence (age key setup, DNS, secrets, provisioning, per-service first-login steps) is documented step by step in `README.md` — follow it rather than re-deriving the sequence from the module files. ## Architecture **Single flake, single host.** `flake.nix` defines exactly one `nixosConfigurations.eurovm` target, pinned to `system = "x86_64-linux"` (switch to `aarch64-linux"` for an ARM Hetzner instance). Inputs are `nixpkgs` (pinned to `nixos-26.05`), `disko`, and `sops-nix`. **Host assembly (`hosts/eurovm/`)** wires together hardware profile, disk layout, and every service module: - `default.nix` — imports everything, sets hostname, the `admin` user + its authorized SSH keys, passwordless sudo for wheel, `system.stateVersion`. - `hardware.nix` — Hetzner virtio/qemu-guest profile, GRUB (BIOS boot), network via DHCP on eth0. - `disko.nix` — declarative disk partitioning consumed by `nixos-anywhere` on first install: a 1MiB BIOS-boot partition, a 512MiB ext4 `/boot`, and the rest as ext4 `/`. **Service modules (`modules/`)** are one file per concern, each self-contained and imported by `hosts/eurovm/default.nix`: - `common.nix` — SSH hardening (key-only, no root login), firewall (22/80/443/53 only), timezone, nix settings (flakes, auto-gc, auto-optimise-store), base packages, disabled autoUpgrade. - `sops.nix` — sops-nix wiring. The host's own age key is derived at boot from `/etc/ssh/ssh_host_ed25519_key`, so no separate host key material needs to be provisioned. Declares which secrets exist, their owning user/group, and file mode. - `caddy.nix` — the only thing that binds to 80/443. Reverse-proxies each subdomain to a service's localhost port. All application ports (3000, 5006, 8080, 8085/8086) are bound to `127.0.0.1` and never exposed directly — Caddy is the sole public HTTP(S) entry point and handles Let's Encrypt automatically. - `adguard.nix` — AdGuard Home; DNS on `0.0.0.0:53`, web UI on `127.0.0.1:3000`. Runs with `mutableSettings = false` (fully declarative config), which creates a specific problem: the module's own `ExecStartPre` overwrites the state file with a config containing a literal `REPLACED_AT_ACTIVATION` placeholder on every start. A second `ExecStartPre`, ordered with `lib.mkAfter`, substitutes in the real bcrypt hash from the sops secret immediately before `ExecStart`. It also uses a static (non-`DynamicUser`) system user, because sops-nix chowns the secret to that user during activation while the service is stopped — a `DynamicUser` doesn't exist at that point. If you touch this module, preserve that ordering; it's the whole point of the file. - `miniflux.nix` — RSS reader; admin account seeded from a sops credentials file (`ADMIN_USERNAME` / `ADMIN_PASSWORD` format) on first run. Uses the NixOS module's own PostgreSQL integration. - `actual.nix` — Actual Budget, run as a Podman OCI container (`virtualisation.oci-containers`) rather than a native NixOS module, because none exists for this nixpkgs version. No secrets involved; the server password is set interactively on first browser visit. - `cgit.nix` — git hosting. Three-way access split: web browsing is public/unauthenticated via cgit, `git clone`/`pull` over HTTPS is public/read-only via `git-http-backend` + fcgiwrap, and `git push` is SSH-only through a dedicated `git` system user with its own authorized keys. Both cgit and git-http-backend actually run behind a minimal localhost-only nginx instance, which Caddy then proxies to — nginx exists here purely as an implementation detail for cgit/fcgiwrap, not as the public-facing server. **Secrets (`secrets/secrets.yaml` + `.sops.yaml`)** — encrypted with sops using age keys. Three recipients: two personal keys (`einstein`, `galileo` — the user's two machines) plus the host's own key (`eurovm`, derived from its SSH host key), so the file can be decrypted both from a laptop and by the running server. When adding a new secret, declare it in `modules/sops.nix` (owner/group/mode/restartUnits) before referencing its `config.sops.secrets."".path` in a service module. ## Conventions to preserve - Every module takes only the NixOS module arguments it actually uses (`{ config, pkgs, lib, ... }` etc.) — don't pad the argument set. - New services follow the same shape: bind the app to `127.0.0.1:`, add a `virtualHosts` entry in `caddy.nix`, and if the service needs a persistent secret, declare it in `modules/sops.nix` first. - Comments in modules explain *why* a workaround exists (e.g. the adguard password-injection dance, the nginx shim in cgit) — keep that context when editing those files rather than stripping it.