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.
96 lines
6.3 KiB
Markdown
96 lines
6.3 KiB
Markdown
# 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_<service>_1` (e.g. `caddy`, `foundry`, `app`, `postgres`, the `authentik-*`/`famapp-*` sets). Prefer `dc <cmd> <service>` 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/<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
|