Every service now has an explicit container_name, eliminating the htpc-download-box_ prefix/suffix that compose derived from the directory. Disambiguated duplicates follow type_context naming: postgres_resume / postgres_famapp / postgres_authentik minio_resume / minio_famapp redis_authentik Also updates CLAUDE.md and the homelab-ssh skill to reflect: - compose v2 (docker compose, space) replacing the old v1.27.4 standalone - dc alias updated to docker compose - htpc-download-box directory is now clean (legacy cruft removed) Server-side migration steps (docker-compose-plugin apt install, alias update, Docker Engine upgrade, old binary removal) are documented in the session but not yet applied — run apply-compose.ps1 -Compose after completing them.
6.2 KiB
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 -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.envonly-Caddy— pushCaddyfile, then hot-reload Caddy (no downtime)-Compose— pushdocker-compose.yml, thendc 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:
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 thedocker-compose-pluginapt package and kept current via apt. Project name ishtpc-download-box(from the directory — inconsequential since every service now has an explicitcontainer_name:). - Container names: every service has an explicit
container_name:. Disambiguated duplicates usetype_contextnaming:postgres_resume,postgres_famapp,postgres_authentik,minio_resume,minio_famapp,redis_authentik. Always use the compose service name withdccommands — container names are just fordocker psreadability. ${ROOT}(.env, default/) prefixes some media-stack volume mounts; other services hardcode/configand/storage1. Both forms resolve to the same real paths.- Networks segment the stack:
foundry,media,media_external,owncloud,resume,famapp,authentik.caddyis the only container on all of them — a new service that Caddy must proxy has to share a network withcaddy. - Persistence lives on the server, not in this repo: container state is in
/config/<service>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<name>.ginnoir.com → reverse_proxy <service>:<port>. - The
(internal_only)snippet 403s any client outside192.168.1.0/24.import internal_onlyis 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)
- Add the service to
docker-compose.yml; put it on a networkcaddyis also on if it needs proxying; bind config to/config/<service>. - Add any secrets/vars to
.env. - Add a site block to
Caddyfile(reverse_proxy <service>:<port>); addimport internal_onlyfor LAN-only access. - Run
apply-compose.ps1(or-Compose -Caddy), then verify withdc ps/dc logs <service>.
Known quirks / gotchas
- A GateGuard hook blocks the first use of
Bash, and everyWrite/Edit, until you state the required facts (the user request + what the operation does/affects). State them, then retry the same call. Caddyfileproxiesmatrix.ginnoir.com → localhost:8008, but there is no Matrix/Synapse service in compose — it's external/legacy. Likewisedev.ginnoir.com → 192.168.1.74:3000points at a different LAN host.- Reactive Resume's
appreads${RESUME_AUTH_SECRET}, which is not defined in.env(resolves empty)..envalso definesAUTH_SECRETthree times — last definition wins (famapp's). watchtowerauto-updates:latestimages, so a running image can drift ahead of what the lastapplypulled.- Line endings: a
.gitattributesforces LF so files stay Unix-clean. Pushing CRLF (especially in.env) to the Linux host would append stray\rto values and break things.
Skills
Project skills in .claude/skills/ wrap the recurring operations:
homelab-sync— pull prod configs into the repohomelab-apply— push config + apply to the live stackhomelab-ssh— run commands / inspect state on valhalla