aboutsummaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md120
1 files changed, 120 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..35a6923
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,120 @@
+# 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@<server-ipv4> \
+ --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@<server-ipv4> \
+ --build-host admin@<server-ipv4> \
+ --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@<server-ipv4>
+```
+
+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."<name>".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:<port>`, 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.