docs: document gitea-first homelab deploys

Switch CLAUDE.md and AGENTS.md to Gitea-primary language. Remove
github-runner from the management plane now that Gitea Actions handles
Caddy reloads and famapp builds. Disable GitHub deploy workflow.
Update apply-compose.ps1 status text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ginnoir
2026-06-15 00:38:15 -05:00
co-authored by Claude Sonnet 4.6
parent e3937dcf52
commit a44adc4814
6 changed files with 24 additions and 53 deletions
+13 -10
View File
@@ -6,9 +6,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
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`).
**The repo is canonical.** Portainer polls the **Gitea** repo `https://gitea.ginnoir.com/ginnoir/homelabstack` on `main` every 5 min and redeploys any application stack whose source files have changed. Editing here changes nothing until you push (or, for the management plane, run `apply-compose.ps1 -Portainer`). GitHub is a temporary push mirror only.
`.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.
`.env` and `stacks/*/stack.env` are committed **intentionally** — secrets are versioned in the private repos. Do not scrub or gitignore them.
## Layout
@@ -25,25 +25,26 @@ Deployment configuration for a **live, single-host Docker homelab** running on a
│ ├── notify/ # ntfy, freshrss, vigilant
│ ├── monitoring/ # uptime-kuma, homarr
│ ├── remote/ # rustdesk hbbr + hbbs
│ └── dev/ # gitea, code-server, registry, bookstack, dbx, plane
│ └── dev/ # code-server, 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)
├── Caddyfile # caddy config (deployed via Gitea Actions 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
── .gitea/workflows/deploy-caddy.yml # Gitea Actions: pushes Caddyfile + reloads caddy on push
└── .github/workflows/deploy.yml # disabled — Gitea Actions is primary
```
## 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) |
| `stacks/<domain>/*` | **git push to Gitea** → Portainer polls every 5 min and redeploys that one stack | ≤ 5 min |
| `Caddyfile` | git push → Gitea Actions copies to `/config/caddy/Caddyfile` + `docker exec caddy caddy reload` | seconds |
| `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.
Gitea is primary; GitHub is a temporary push mirror. Portainer app stacks must have **no Portainer UI env vars** — all config comes from `stack.env` via `env_file: stack.env`.
## On-host topology
@@ -91,7 +92,7 @@ ssh ginnoir@valhalla "docker compose -f /config/portainer/compose/14/stacks/ownc
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). **Env convention:** every service gets `env_file: stack.env`, and var names in `stack.env` are exactly what the container reads — no `${VAR}` interpolation, no Portainer UI env vars. Composite values (e.g. `DATABASE_URL`) are written out in full in `stack.env`; keep embedded components in sync on rotation. Exception: `stacks/dev` still uses `${VAR}` interpolation backed by Portainer UI env vars — when editing its `stack.env`, mirror the change in the stack's UI env vars too.
3. Add secrets to `stacks/<domain>/stack.env` (Portainer reads this file directly from the git repo on each redeploy). **Env convention:** every service gets `env_file: stack.env`, and var names in `stack.env` are exactly what the container reads — no `${VAR}` interpolation, no Portainer UI env vars. Composite values (e.g. `DATABASE_URL`) are written out in full in `stack.env`; keep embedded components in sync on rotation.
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.
@@ -113,7 +114,9 @@ ssh ginnoir@valhalla "docker compose -f /config/portainer/compose/14/stacks/ownc
- `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. **Infra images** (Postgres, MariaDB, Redis, MinIO, CouchDB, Caddy, Vault) are **version-pinned** in compose with `com.centurylabs.watchtower.enable=false`; bump tags manually every few months.
- 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`.
- **Caddyfile reloads** are handled by `.gitea/workflows/deploy-caddy.yml` (Gitea Actions, `valhalla-management-runner`). If Gitea Actions doesn't trigger, fall back to `apply-compose.ps1 -Caddy`. The GitHub workflow (`.github/workflows/deploy.yml`) is disabled.
- **famapp images** are built by Gitea Actions on `v*` tags and pushed to `registry.ginnoir.com/ginnoir/famapp`. Portainer pulls from the self-hosted registry (registered in Portainer as "Valhalla Registry").
- **Portainer app stacks must have empty UI environment variables.** All stack config comes from `stack.env` via `env_file`. Run `scripts/check-portainer-stack-env.ps1` to verify before pushing.
## Skills