feat: multi-server deployment topology and CI fan-out #5

Open
shcizo wants to merge 6 commits from feat/multi-server-fanout into main
2 changed files with 35 additions and 7 deletions
Showing only changes of commit 81bd01c5d6 - Show all commits
+8 -3
View File
@@ -47,9 +47,14 @@ curl -sH "Authorization: Bearer $UPDATER_API_KEY" \
Tests rely on this — don't drop the nil-check. Tests rely on this — don't drop the nil-check.
- **Stateless**: no DB, no config file, no on-disk audit log. Docker daemon is - **Stateless**: no DB, no config file, no on-disk audit log. Docker daemon is
the source of truth. the source of truth.
- **Compose mode and Swarm mode are selected once per deployment via `MODE`**, never - **`MODE` is exclusive because each instance owns exactly one Docker daemon.**
mixed at request time. Swarm mode's security gate is opt-in label only — there is A composite "handle both at once" mode has been considered and rejected: it would
no STACKS_ROOT-equivalent path check, since Swarm services have no local compose still only reach one daemon, so it buys nothing. Fleets run one instance per
server and CI fans out. Reopen this only if a Swarm manager node starts running
standalone Compose stacks locally. Rationale and rejected alternatives:
`docs/superpowers/specs/2026-08-04-multi-server-fanout-design.md`.
- **Swarm mode's security gate is opt-in label only** — there is no
STACKS_ROOT-equivalent path check, since Swarm services have no local compose
file. Don't add one; don't weaken Compose mode's three-factor gate to match. file. Don't add one; don't weaken Compose mode's three-factor gate to match.
## Gotchas ## Gotchas
+27 -4
View File
@@ -46,6 +46,28 @@ Two things to get right when running in Swarm mode:
mounted. This is a deployment concern the service cannot detect or work mounted. This is a deployment concern the service cannot detect or work
around. around.
## Deploying across multiple servers
An instance can only reach the Docker daemon it is configured against. It cannot
update stacks on other machines. The deployment model follows from that:
**One instance per server.** Each server runs its own package-updater against its
own local Docker socket, with `MODE` set to whatever that machine runs:
| Server | `MODE` | Updates |
|---|---|---|
| Swarm manager node | `swarm` | All Swarm services in the cluster |
| Standalone Compose host | `compose` | Compose stacks on that host |
**CI fans out.** The [Gitea composite action](gitea-action/README.md) takes a list of
endpoints and posts `/update` to every instance, so one workflow run reaches the
whole fleet. Each instance answers for its own machine; there is no aggregated
cross-server response and no instance coordinates any other.
This is why `MODE` is exclusive rather than a mode that handles both at once: an
instance that could do both would still only reach one daemon, so the extra
generality buys nothing.
## Quick start ## Quick start
1. Build and push the image (e.g. via your own CI). 1. Build and push the image (e.g. via your own CI).
@@ -68,7 +90,7 @@ All via environment variables.
| `LOG_LEVEL` | no | `info` | `debug` / `info` / `warn` / `error`. | | `LOG_LEVEL` | no | `info` | `debug` / `info` / `warn` / `error`. |
| `UPDATE_TIMEOUT` | no | `5m` | Per-job timeout (Go duration). | | `UPDATE_TIMEOUT` | no | `5m` | Per-job timeout (Go duration). |
| `OPT_IN_LABEL` | no | `se.shcizo.auto-update` | Label name to check; value must equal `"true"`. | | `OPT_IN_LABEL` | no | `se.shcizo.auto-update` | Label name to check; value must equal `"true"`. |
| `MODE` | no | `compose` | `compose` or `swarm`. Selects the update mechanism for the whole deployment; not mixed per-request. | | `MODE` | no | `compose` | `compose` or `swarm`. Which paradigm *this instance's* Docker daemon runs. See [Deploying across multiple servers](#deploying-across-multiple-servers). |
## Endpoints ## Endpoints
@@ -100,7 +122,8 @@ These are tracked in the spec's section 2 and section 15 as deliberate out-of-sc
- **Self-update wiring**: `internal/selfupdate.Wrapped` exists and is unit-tested but is not wired into the live queue. The HTTP response flush ordering for self-replacement is a future enhancement; for now, expect to manually rerun `docker compose up -d` on the host if pushing a new image of `package-updater` itself causes a mid-response interruption. - **Self-update wiring**: `internal/selfupdate.Wrapped` exists and is unit-tested but is not wired into the live queue. The HTTP response flush ordering for self-replacement is a future enhancement; for now, expect to manually rerun `docker compose up -d` on the host if pushing a new image of `package-updater` itself causes a mid-response interruption.
- **No rollback**: Compose's "keep old container if new fails to start" is the only safety net. - **No rollback**: Compose's "keep old container if new fails to start" is the only safety net.
- **Single host only in Compose mode**. Swarm mode (`MODE=swarm`) is the - **One instance reaches one daemon.** An instance never updates another server;
multi-node path, but only from a manager node's point of view — the updater fleets run one instance per server with CI fanning out to all of them (see
itself still needs manager API access (see "Swarm mode" above). "Deploying across multiple servers"). Swarm mode is the exception in that a
single manager-node instance covers the whole cluster.
- **No per-repo API keys**: a single shared bearer token is used. - **No per-repo API keys**: a single shared bearer token is used.