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
+5 -21
View File
@@ -1,30 +1,14 @@
name: Deploy to valhalla
# Post-split deploy. The application stacks (foundry, media, notify, monitoring,
# remote, owncloud, resume, famapp, authentik, dev) live under stacks/<domain>/
# and are deployed by Portainer git polling — the runner does NOT touch them.
# Caddy lives in its own Portainer git stack (stacks/proxy/) and is likewise
# deployed by Portainer; this workflow only pushes the Caddyfile and triggers
# a hot reload on the running Caddy container.
# Deployment moved to Gitea Actions (.gitea/workflows/deploy-caddy.yml).
# This workflow is kept as a manual fallback only.
on:
push:
branches: [main]
paths:
- Caddyfile
- .github/workflows/deploy.yml
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
workflow_dispatch:
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Push Caddyfile and reload Caddy
run: |
cp Caddyfile /config/caddy/Caddyfile
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
- name: Disabled
run: echo "Deployment moved to Gitea Actions."
+4 -2
View File
@@ -15,9 +15,11 @@ from your tool's user-level instructions (`~/.codex/AGENTS.md` / `~/.gemini/GEMI
- **The repo is canonical.** Portainer polls `main` every 5 min and redeploys any app stack whose
`stacks/<domain>/*` files changed. Editing here changes nothing until you `git push`.
- **Deployment channels:**
- `stacks/<domain>/*` → git push → Portainer redeploys that stack (≤ 5 min).
- `Caddyfile` → git push → runner workflow `scp`s it + reloads Caddy (or `apply-compose.ps1 -Caddy`).
- `stacks/<domain>/*` → git push to Gitea → Portainer polls every 5 min and redeploys that stack. GitHub is a temporary mirror only.
- `Caddyfile` → git push → Gitea Actions (`.gitea/workflows/deploy-caddy.yml`) copies it + reloads Caddy. Fallback: `apply-compose.ps1 -Caddy`.
- `portainer-compose.yml` / `vault.hcl``apply-compose.ps1 -Portainer` (Portainer can't manage itself).
- **Portainer app stacks must have empty UI environment variables.** All config comes from `stack.env` via `env_file: stack.env`. Run `scripts/check-portainer-stack-env.ps1` before pushing.
- **famapp images** build on Gitea Actions (`v*` tags) and push to `registry.ginnoir.com/ginnoir/famapp`.
- **Secrets are committed intentionally.** `.env` and `stacks/*/stack.env` are versioned in this
PRIVATE repo. Do **not** scrub or gitignore them.
- **Line endings:** `.gitattributes` forces LF. Never push CRLF (especially `stack.env`) — stray
+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
+1 -1
View File
@@ -53,7 +53,7 @@ if ($Portainer) {
Write-Host "Pushing vault.hcl (Vault lives in the management plane) ..."
ssh $server "mkdir -p /config/vault/config /config/vault/data /config/vault/logs"
scp $vaultConfigLocal "${server}:/config/vault/config/vault.hcl"
Write-Host "Applying management plane (portainer, vault, github-runner, watchtower) ..."
Write-Host "Applying management plane (portainer, vault, gitea, registry, watchtower) ..."
ssh $server "docker compose -f ~/valhalla-lab/portainer-compose.yml --env-file ~/valhalla-lab/.env up -d"
}
-18
View File
@@ -187,24 +187,6 @@ services:
labels:
- "com.centurylabs.watchtower.enable=false"
github-runner:
container_name: github_runner
image: myoung34/github-runner:latest
restart: unless-stopped
environment:
- ACCESS_TOKEN=${GITHUB_RUNNER_ACCESS_TOKEN}
- REPO_URL=https://github.com/ginnoir/homelabstack
- RUNNER_NAME=valhalla
- RUNNER_LABELS=self-hosted,Linux,x64
- RUNNER_SCOPE=repo
- DOCKER_GROUP_GID=1001
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/ginnoir/valhalla-lab:/valhalla-lab
- /config/caddy:/config/caddy
labels:
- "com.centurylabs.watchtower.enable=false"
watchtower:
container_name: watchtower
image: containrrr/watchtower:latest
+1 -1
View File
@@ -7,7 +7,7 @@
#
# The Caddyfile itself is unchanged — upstreams are service names that resolve
# over edge. It is bind-mounted from /config/caddy/Caddyfile and hot-reloaded
# by the GitHub Actions runner on push (deploy.yml).
# by the Gitea Actions runner on push (.gitea/workflows/deploy-caddy.yml).
#
# serfriz/caddy-cloudflare-ddns bundles caddy-dns/cloudflare (DNS-01 ACME),
# caddy-cloudflare-ip (real client IPs behind CF), caddy-dynamicdns.