Portainer git-stack deploys require stack.env (not .env) in the repo. Renames all stacks/*/. env → stacks/*/stack.env so Portainer reads secrets directly from git on each redeploy, making the repo the single source of truth instead of requiring manual UI sync. Also fixes the dev stack BOOKSTACK_APP_KEY gap — the key was already present in the file but missing from Portainer's stored envVars; it will now be picked up automatically from stack.env on next redeploy. Updates CLAUDE.md to reflect the new filename convention.
121 lines
7.8 KiB
Markdown
121 lines
7.8 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 the canonical source for ~50 containers organized into per-domain Portainer-managed stacks.
|
|
|
|
**The repo is canonical.** Portainer polls this repo's `main` branch every 5 min and redeploys any application stack whose source files have changed. Editing here changes nothing until you push to GitHub (or, for the management plane, run `apply-compose.ps1 -Portainer`).
|
|
|
|
`.env` and `stacks/*/stack.env` are committed **intentionally** — the GitHub repo `ginnoir/homelabstack` is private and the stack's secrets are versioned with it. Do not scrub or gitignore them.
|
|
|
|
## Layout
|
|
|
|
```
|
|
.
|
|
├── stacks/ # one directory per Portainer-managed application stack
|
|
│ ├── proxy/ # caddy
|
|
│ ├── media/ # *arr stack, deluge, qbittorrent, stash, ...
|
|
│ ├── foundry/ # foundry, foundry2, 5etools
|
|
│ ├── owncloud/ # owncloud + its mariadb + redis
|
|
│ ├── resume/ # reactive-resume + its postgres + minio + chrome
|
|
│ ├── famapp/ # famapp + its postgres + minio
|
|
│ ├── authentik/ # authentik server/worker + its postgres + redis
|
|
│ ├── notify/ # ntfy, freshrss, vigilant
|
|
│ ├── monitoring/ # uptime-kuma, homarr
|
|
│ ├── remote/ # rustdesk hbbr + hbbs
|
|
│ └── dev/ # gitea, code-server, registry, bookstack, dbx, plane
|
|
├── portainer-compose.yml # management plane (raw compose, not a Portainer stack)
|
|
├── vault.hcl # vault config (deployed alongside portainer-compose.yml)
|
|
├── Caddyfile # caddy config (deployed via runner workflow or -Caddy)
|
|
├── .env # mgmt-plane env + vault unseal keys + global vars
|
|
├── apply-compose.ps1 # manual ops helper (mgmt plane + Caddyfile + vault unseal)
|
|
└── .github/workflows/deploy.yml # runner: pushes Caddyfile + reloads caddy on push
|
|
```
|
|
|
|
## Deployment channels — what triggers what
|
|
|
|
| Edit | Deploys via | Latency |
|
|
|---|---|---|
|
|
| `stacks/<domain>/*` | **git push** → Portainer polls every 5 min and redeploys that one stack | ≤ 5 min |
|
|
| `Caddyfile` | git push → runner workflow `scp`s to `/config/caddy/Caddyfile` + `docker exec caddy caddy reload` | seconds (when workflow is enabled) |
|
|
| `portainer-compose.yml` / `vault.hcl` | `apply-compose.ps1 -Portainer` (Portainer can't manage itself) | seconds |
|
|
| `~/valhalla-lab/.env` (mgmt-plane env, vault unseal keys) | `apply-compose.ps1 -EnvFile` | seconds |
|
|
|
|
The runner workflow gate-filters on `paths: [Caddyfile, .github/workflows/deploy.yml]` so non-Caddyfile pushes don't trigger it.
|
|
|
|
## On-host topology
|
|
|
|
| Path on valhalla | Purpose |
|
|
|---|---|
|
|
| `~/valhalla-lab/portainer-compose.yml` + `.env` | management plane (raw compose; project name `valhalla-lab`) |
|
|
| `/data/compose/<id>/stacks/<domain>/` | Portainer's working copy of each app stack (auto-pulled from git) |
|
|
| `/config/<service>/` | SSD-tier persistence: app configs, Postgres data, Redis data |
|
|
| `/storage1/labdata/<service>/` | ZFS-tier persistence: MinIO blob buckets, registry image layers, gitea repos/LFS |
|
|
| `/storage1/` | media library (Books, Tabletop, plex-style media) |
|
|
| `/config/caddy/Caddyfile` | live Caddy config (deployed from this repo) |
|
|
| `/config/vault/` | Vault config + sealed data + audit logs |
|
|
|
|
The pre-split monolith's named volumes (`htpc-download-box_*` and `valhalla-lab_*`) are still on the host as a safety net — every app now uses bind mounts. Reap the orphans once you trust the new persistence.
|
|
|
|
## Networks
|
|
|
|
Single shared edge network, not Caddy-on-every-network:
|
|
|
|
- `edge` — the **only** reverse-proxy network. Caddy + everything Caddy proxies. A new public service must join this.
|
|
- `<domain>` (e.g. `media`, `authentik`, `dev`) — per-stack private network for intra-stack traffic (app ↔ its db/cache/minio).
|
|
- `portainer_proxy`, `valhalla-lab_default` — mgmt plane (`valhalla-lab_default` is auto-named after the project dir; it's mis-named but legitimate).
|
|
- `bridge`, `host`, `none` — Docker defaults.
|
|
|
|
`reverse_proxy` upstreams use container name + container-internal port (e.g. `qbittorrent:3232`, `stash:6970`), resolving over `edge`.
|
|
|
|
## Common ops on valhalla
|
|
|
|
There is **no `dc` alias** anymore. Each stack has its own compose dir. Use plain `docker` against container names:
|
|
|
|
```powershell
|
|
ssh -o BatchMode=yes ginnoir@valhalla "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Label \"com.docker.compose.project\"}}'"
|
|
ssh -o BatchMode=yes ginnoir@valhalla "docker logs --tail 200 -f <container>"
|
|
ssh -o BatchMode=yes ginnoir@valhalla "docker exec caddy caddy reload --config /etc/caddy/Caddyfile"
|
|
```
|
|
|
|
For per-stack compose ops, pick the file from the container's label:
|
|
|
|
```powershell
|
|
ssh ginnoir@valhalla "docker inspect <container> --format '{{ index .Config.Labels \"com.docker.compose.project.config_files\"}}'"
|
|
ssh ginnoir@valhalla "docker compose -f /data/compose/14/stacks/owncloud/docker-compose.yml ps"
|
|
```
|
|
|
|
## Adding a service
|
|
|
|
1. Pick the stack it belongs to under `stacks/<domain>/` (or create a new stack and a new Portainer git stack for it).
|
|
2. Add the service to `stacks/<domain>/docker-compose.yml`. Configs → `/config/<svc>` bind. Blobs → `/storage1/labdata/<svc>` bind. Add `edge` to its `networks:` if Caddy must reach it; otherwise just the per-stack private net.
|
|
3. Add secrets to `stacks/<domain>/stack.env` (Portainer reads this file directly from the git repo on each redeploy).
|
|
4. Add a site block to `Caddyfile` (`reverse_proxy <container_name>:<port>`); `import internal_only` for LAN-only.
|
|
5. Run `./scripts/gen-bookmarks.ps1` to regenerate `bookmarks-domains.html` + `bookmarks-ports.html` from the new Caddy block / published ports.
|
|
6. `git push`. Portainer redeploys the stack within 5 min; the runner reloads Caddy on the same push if `Caddyfile` changed.
|
|
|
|
## Caddyfile
|
|
|
|
- TLS uses the Cloudflare DNS-01 challenge (`acme_dns cloudflare {env.CF_API_TOKEN}`), so certs need no inbound ports.
|
|
- 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.
|
|
- Upstreams use container name + container-internal port (e.g. `qbittorrent:3232`), not host-published ports.
|
|
|
|
## 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.
|
|
- `watchtower` auto-updates `:latest` images, so a running image can drift ahead of what the last `apply` pulled.
|
|
- Line endings: `.gitattributes` forces **LF** so files stay Unix-clean. Pushing CRLF (especially `stack.env`) to the Linux host appends stray `\r` to values and breaks things.
|
|
- The runner workflow (`.github/workflows/deploy.yml`) may be **disabled** at the GitHub repo level — check `gh workflow list` if Caddyfile pushes don't trigger a reload. Manual fallback: `apply-compose.ps1 -Caddy`.
|
|
|
|
## Skills
|
|
|
|
Project skills in `.claude/skills/` wrap the recurring ops:
|
|
|
|
- `homelab-apply` — deploy changes (git push for app stacks; `apply-compose.ps1` for mgmt plane / Caddyfile / vault)
|
|
- `homelab-ssh` — inspect and operate the live containers from SSH
|
|
|
|
The pre-split `homelab-sync` skill (pull live config into repo) has been retired — the repo is now canonical, not a mirror.
|