# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this repo is Deployment configuration for a **live, single-host Docker homelab** running on a headless Ubuntu server reachable at `ssh ginnoir@valhalla`. This repo is **not application code** — it is a version-controlled mirror of three files that live on the server. Editing here changes nothing until the files are pushed to valhalla and the stack is re-applied. Tracked files ↔ where they live on valhalla: | Repo file | On valhalla | Consumed by | |-----------|-------------|-------------| | `docker-compose.yml` | `~/htpc-download-box/docker-compose.yml` | the whole stack | | `.env` | `~/htpc-download-box/.env` | compose variable substitution | | `Caddyfile` | `/config/caddy/Caddyfile` | the `caddy` container, mounted at `/etc/caddy/Caddyfile` | `.env` is committed **intentionally** — the GitHub repo `ginnoir/homelabstack` is private and the stack's secrets are versioned with it. Do not scrub or gitignore it. The server's `~/htpc-download-box/` contains only `docker-compose.yml` and `.env` — the legacy cruft from the original *htpc-download-box* project has been cleaned out. ## The core workflow Production is the source of truth. **Sync before editing, apply after editing:** ```powershell powershell -File sync-prod.ps1 # pull prod -> repo (OVERWRITES local copies) # ...edit docker-compose.yml / Caddyfile / .env... powershell -File apply-compose.ps1 # push repo -> prod and apply (mutates the LIVE stack) ``` `apply-compose.ps1` flags (combine freely; no flags = all three): - `-EnvFile` — push `.env` only - `-Caddy` — push `Caddyfile`, then hot-reload Caddy (no downtime) - `-Compose` — push `docker-compose.yml`, then `dc pull` + `dc up -d` Caddy changes hot-reload; on `dc up -d` only containers whose resolved config changed are recreated. Cosmetic/whitespace-only compose edits are therefore no-ops to the running containers. ## Running commands on valhalla SSH is key-based and works from this Windows host directly. In automation, pass `-o BatchMode=yes` so it fails fast instead of hanging on a prompt. All compose operations go through the **`dc` alias** in the server's `~/.bashrc`: ``` dc = docker compose -f ~/htpc-download-box/docker-compose.yml --env-file ~/htpc-download-box/.env ``` It's a shell alias, so it only exists in an **interactive** shell — invoke it over SSH with `bash -ic`: ```powershell ssh ginnoir@valhalla "bash -ic 'dc ps'" ssh ginnoir@valhalla "bash -ic 'dc logs -f --tail=100 sonarr'" ssh ginnoir@valhalla "bash -ic 'dc restart caddy'" ssh ginnoir@valhalla "bash -ic 'dc exec caddy caddy reload --config /etc/caddy/Caddyfile'" ``` Address services by their **compose service name** (`sonarr`, `caddy`, `app`, …) — this works regardless of how the container is named. ## docker-compose.yml — read before editing - **Compose v2** (`docker compose`, space), installed as the `docker-compose-plugin` apt package and kept current via apt. Project name is `htpc-download-box` (from the directory — inconsequential since every service now has an explicit `container_name:`). - **Container names:** every service has an explicit `container_name:`. Disambiguated duplicates use `type_context` naming: `postgres_resume`, `postgres_famapp`, `postgres_authentik`, `minio_resume`, `minio_famapp`, `redis_authentik`. Always use the compose **service name** with `dc` commands — container names are just for `docker ps` readability. - **`${ROOT}`** (`.env`, default `/`) prefixes some media-stack volume mounts; other services hardcode `/config` and `/storage1`. Both forms resolve to the same real paths. - **Networks segment the stack:** `foundry`, `media`, `media_external`, `owncloud`, `resume`, `famapp`, `authentik`. `caddy` is the only container on all of them — a new service that Caddy must proxy has to share a network with `caddy`. - **Persistence lives on the server, not in this repo:** container state is in `/config/` bind mounts, under `/storage1`, or in named volumes (`postgres_data`, `minio_data`, `authentik_db_data`, `garden_uploads`, …). ## Caddyfile - TLS uses the Cloudflare DNS-01 challenge (`acme_dns cloudflare {env.CF_API_TOKEN}`), so certs need no inbound ports. Most sites are `.ginnoir.com → reverse_proxy :`. - The `(internal_only)` snippet 403s any client outside `192.168.1.0/24`. `import internal_only` is how admin UIs (sonarr, qbittorrent, …) stay LAN-only while public sites omit it. - Reverse-proxy upstreams use the compose **service name + container-internal port** (e.g. `qbittorrent:3232`, `stash:6970`), not the host-published port. ## Adding a service (the common change) 1. Add the service to `docker-compose.yml`; put it on a network `caddy` is also on if it needs proxying; bind config to `/config/`. 2. Add any secrets/vars to `.env`. 3. Add a site block to `Caddyfile` (`reverse_proxy :`); add `import internal_only` for LAN-only access. 4. Run `apply-compose.ps1` (or `-Compose -Caddy`), then verify with `dc ps` / `dc logs `. ## Known quirks / gotchas - A **GateGuard hook blocks the first use of `Bash`, and every `Write`/`Edit`, until you state the required facts** (the user request + what the operation does/affects). State them, then retry the same call. - `Caddyfile` proxies `matrix.ginnoir.com → localhost:8008`, but there is **no Matrix/Synapse service in compose** — it's external/legacy. Likewise `dev.ginnoir.com → 192.168.1.74:3000` points at a different LAN host. - Reactive Resume's `app` reads `${RESUME_AUTH_SECRET}`, which is **not defined in `.env`** (resolves empty). `.env` also defines `AUTH_SECRET` three times — last definition wins (famapp's). - `watchtower` auto-updates `:latest` images, so a running image can drift ahead of what the last `apply` pulled. - Line endings: a `.gitattributes` forces **LF** so files stay Unix-clean. Pushing CRLF (especially in `.env`) to the Linux host would append stray `\r` to values and break things. ## Skills Project skills in `.claude/skills/` wrap the recurring operations: - `homelab-sync` — pull prod configs into the repo - `homelab-apply` — push config + apply to the live stack - `homelab-ssh` — run commands / inspect state on valhalla