Mirror the three production files (docker-compose.yml, .env, Caddyfile) that live on valhalla, plus the push/pull PowerShell scripts, CLAUDE.md, .gitignore/.gitattributes, and .claude/skills for ssh/apply/sync.
6.3 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/ 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 -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 v1.27.4. The binary is hyphenated
docker-compose;docker compose(v2) is not installed. Project name ishtpc-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 patternhtpc-download-box_<service>_1(e.g.caddy,foundry,app,postgres, theauthentik-*/famapp-*sets). Preferdc <cmd> <service>so you don't have to track which is which. ${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