# 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/` also holds unrelated legacy files (`*.bak`, a 2020 `.git`, an old 42 KB `README.md`, `Vagrantfile`, `apache/`, `proxy/`): this stack was grafted onto the original *htpc-download-box* project. Leave that cruft alone — only the three files above are managed from here. ## 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 v1.27.4.** The binary is hyphenated `docker-compose`; `docker compose` (v2) is **not** installed. Project name is `htpc-download-box` (from the directory). - **Container names:** services with an explicit `container_name:` (most of the media stack, `owncloud_*`, `ntfy`, `freshrss`, `hbbr`/`hbbs`) use that literal name. Services without one get the v1 pattern `htpc-download-box__1` (e.g. `caddy`, `foundry`, `app`, `postgres`, the `authentik-*`/`famapp-*` sets). Prefer `dc ` so you don't have to track which is which. - **`${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