Files
homelabstack/CLAUDE.md
T
ginnoir 20165f48a0 chore: explicit container_name on every service, document compose v2 migration
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.
2026-06-02 22:58:23 -05:00

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 .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:

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/<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 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/<service>.
  2. Add any secrets/vars to .env.
  3. Add a site block to Caddyfile (reverse_proxy <service>:<port>); add import internal_only for LAN-only access.
  4. Run apply-compose.ps1 (or -Compose -Caddy), then verify with dc ps / dc logs <service>.

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