148 lines
6.6 KiB
Markdown
148 lines
6.6 KiB
Markdown
# Strategy
|
|
|
|
## Pattern
|
|
|
|
Pull-based GitOps via Gitea Actions runners on the owner host.
|
|
|
|
```
|
|
dev box ── git push ──► Gitea (Charlie/<stack>) ── webhook ──► runner on owner host
|
|
│
|
|
▼
|
|
git -C /mnt/nas/stacks/<stack> pull --ff-only
|
|
docker compose pull && docker compose up -d
|
|
```
|
|
|
|
The git working tree at `/mnt/nas/stacks/<stack>/` **is** the deployment. Runtime state lives separately under `/mnt/nas/data/<stack>/` (see [layout.md](layout.md)).
|
|
|
|
## Rules
|
|
|
|
- One owner host per stack (singleton stacks). Only that host's runner ever touches the working tree.
|
|
- Shared stacks (run on multiple hosts — see [Shared stacks](#shared-stacks)) follow the same NFS-shared-tree model but each host runs its own `docker compose up -d` against host-specific overrides.
|
|
- No hand-edits on the host once a stack is repo-managed. `git pull --ff-only` will refuse to clobber.
|
|
- Runtime state never enters git. Compose files bind into `/mnt/nas/data/<stack>/`, not into the working tree.
|
|
- `.env` ignored, `.env.example` tracked. Secrets stay out.
|
|
- Image tags pinned. No `:latest`.
|
|
- Cross-host self-update: the runner that deploys `Charlie/gitea` lives on morgott; the runner that updates morgott's runner lives on melina. A runner never redeploys itself mid-job.
|
|
|
|
## Repo types
|
|
|
|
Three. (We dropped `upstream+overrides` — see [Why no upstream cloning](#why-no-upstream-cloning).)
|
|
|
|
### `init`
|
|
|
|
Stack is currently just a NAS folder. `git init` in the new `/mnt/nas/stacks/<stack>/`, push to `Charlie/<name>`. Recipe: [migration.md](migration.md#init).
|
|
|
|
### `rewrite`
|
|
|
|
Code we author, currently a clone of a github.com repo. Move to `/mnt/nas/stacks/<stack>/`, `git remote set-url origin` to Charlie. Recipe: [migration.md](migration.md#rewrite).
|
|
|
|
### `mirror`
|
|
|
|
Pull-mirror of an upstream we don't customise (or want as offline backup). Configured via Gitea's pull-mirror feature. Read-only, never deployed from. Naming: `Charlie/<name>-mirror`.
|
|
|
|
## Shared stacks
|
|
|
|
Some stacks run on **multiple hosts** with the same image but slightly different configuration (e.g. `otelcollector`, `portainer-agent`). The pattern, borrowed from Ansible inventories + Kustomize overlays + Kubernetes DaemonSets:
|
|
|
|
```
|
|
Charlie/<stack>/
|
|
├── docker-compose.yml # the "what" — default, used by hosts without an override
|
|
├── docker-compose.<host>.yml # full per-host compose (when the delta is structural)
|
|
├── envs/
|
|
│ ├── morgott.env # per-host env (Ansible host_vars equivalent)
|
|
│ ├── melina.env
|
|
│ ├── mohg.env
|
|
│ └── miquella.env
|
|
├── overrides/ # additive overlay deltas (when env can't express it)
|
|
│ └── <host>.yml
|
|
├── deploy.sh # picks env + compose file based on `hostname -s`
|
|
└── README.md
|
|
```
|
|
|
|
### Resolution order in `deploy.sh` (per host)
|
|
|
|
1. If `docker-compose.<host>.yml` exists → use **only** that (full per-host compose).
|
|
2. Else `docker-compose.yml` + (if present) `overrides/<host>.yml` layered on top.
|
|
3. If `envs/<host>.env` exists → loaded with `--env-file` in either case.
|
|
|
|
Hostname is lowercased (`hostname -s | tr '[:upper:]' '[:lower:]'`).
|
|
|
|
### When to use which
|
|
|
|
| Delta type | Use |
|
|
|---|---|
|
|
| Different scalar value (port, env var, URL) | `envs/<host>.env` |
|
|
| Add an extra service or scalar field | `overrides/<host>.yml` (compose merges additively) |
|
|
| **Replace** a list (volumes, command, networks) | `docker-compose.<host>.yml` (full file) |
|
|
| Different image | `docker-compose.<host>.yml` |
|
|
|
|
Compose's `!override` and `!reset` YAML tags exist for list-replacement, but support is uneven across compose versions (notably broken on Synology DSM's bundled compose). Prefer the full-file approach when replacing lists.
|
|
|
|
### Canonical `deploy.sh`
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
HOST=$(hostname -s | tr '[:upper:]' '[:lower:]')
|
|
|
|
if [ -f "docker-compose.$HOST.yml" ]; then
|
|
args=(-f "docker-compose.$HOST.yml")
|
|
else
|
|
args=(-f docker-compose.yml)
|
|
[ -f "overrides/$HOST.yml" ] && args+=(-f "overrides/$HOST.yml")
|
|
fi
|
|
[ -f "envs/$HOST.env" ] && args=(--env-file "envs/$HOST.env" "${args[@]}")
|
|
|
|
exec sudo docker compose "${args[@]}" "$@"
|
|
```
|
|
|
|
### Mental model
|
|
|
|
- "What" = `docker-compose.yml` (or per-host file).
|
|
- "Where" = the host running `./deploy.sh` (no inventory file; the host knows its own name).
|
|
- Per-host vars = `envs/<host>.env`.
|
|
- Per-host structural deltas = `overrides/<host>.yml` for additive, `docker-compose.<host>.yml` for replacement.
|
|
- Group defaults = compose env-var defaults (`${VAR:-default}`).
|
|
|
|
### Once runners exist (Phase 2)
|
|
|
|
Workflow declares a host matrix; one job per host, all parallel:
|
|
|
|
```yaml
|
|
strategy:
|
|
matrix:
|
|
host: [morgott, melina, mohg, miquella]
|
|
runs-on: [self-hosted, "${{ matrix.host }}"]
|
|
steps:
|
|
- run: cd /mnt/nas/stacks/<stack> && ./deploy.sh up -d
|
|
```
|
|
|
|
That's a Kubernetes DaemonSet shape adapted to compose + Gitea Actions.
|
|
|
|
## Why no upstream cloning
|
|
|
|
Previously we considered cloning upstream's source on the host and layering a `docker-compose.override.yml`. Dropped because:
|
|
|
|
- For every "upstream image" stack (`invidious`, `searxng`, `victoriametrics`, `mealie`, `paperless-ngx`), we only ever ran the published image. Their source tree is dead weight on the host.
|
|
- Override-on-template means the upstream's compose can change under us silently.
|
|
- Two-remote / rebase / force-push dance was the price for dubious value.
|
|
|
|
Instead: our `docker-compose.yml` references the published image directly. It is the only compose. Upstream's compose is a *reference document* we read in their mirror repo when we want to see what changed.
|
|
|
|
## Image-update flow (Phase 3)
|
|
|
|
Diun watches registries; on a tag/digest change it POSTs to a Gitea webhook that triggers a `redeploy` workflow with `force-pull: true` on the owner host. Replaces watchtower.
|
|
|
|
## Secrets
|
|
|
|
Open question. Candidate: vaultwarden as the source, with a runner-side fetch step that materialises `.env` per job. Decided when we design the runner stack.
|
|
|
|
## Why not Portainer/Komodo/Dockge
|
|
|
|
- **Portainer Stacks-from-Git**: works, but no submodule support, opinionated about working-tree location, adds a control plane on top of git.
|
|
- **Komodo**: nice, also pull-based, also relocates working trees. Reconsider if we ever want a UI.
|
|
- **Dockge**: file mgmt only, no git wiring.
|
|
|
|
The runner-driven pattern keeps us in plain `git` + `docker compose` and adds nothing the team doesn't already understand.
|