# package-updater Webhook-driven Docker Compose service updater. Fills the gap between Watchtower (polling, no CI integration) and full GitOps (Argo CD, Flux) for a self-hosted, single-host environment. **Trigger flow:** 1. Gitea workflow builds and pushes a new image to your registry. 2. Workflow calls `POST /update` on this service with the image name. 3. Service finds the matching Compose-managed container(s) on the host via Docker labels. 4. Runs `docker compose pull` + `up -d` for the relevant service(s). See [design spec](docs/superpowers/specs/2026-05-22-package-updater-design.md) and [implementation plan](docs/superpowers/plans/2026-05-22-package-updater-implementation.md) for full design and rationale. ## How it finds the right stack The service queries the Docker socket and reads the labels Compose itself attaches to every container: - `com.docker.compose.project` - `com.docker.compose.service` - `com.docker.compose.project.working_dir` - `com.docker.compose.project.config_files` A container is eligible for update only if it has **both**: - An image name matching the request (tag-agnostic), AND - The opt-in label `se.shcizo.auto-update=true`. Defense in depth: a valid bearer token AND the opt-in label must both be present before any container is touched. ## Quick start 1. Build and push the image (e.g. via your own CI). 2. Copy `docker-compose.example.yml` to `/home/shcizo/self-hosted/package-updater/docker-compose.yml`. 3. Create `.env` next to it: `UPDATER_API_KEY=$(openssl rand -hex 32)`. 4. Point your reverse proxy (NPM/Traefik/Caddy) at `package-updater:8080`. NPM should handle TLS. 5. `docker compose up -d`. 6. Add the opt-in label `se.shcizo.auto-update: "true"` to each service you want auto-updated. 7. Use the [Gitea composite action](https://gitea.shcizo.se/shcizo/package-updater-action) in your repos to call `/update` after a build. ## Configuration All via environment variables. | Variable | Required | Default | Purpose | |---|---|---|---| | `UPDATER_API_KEY` | **yes** | — | Bearer token. Service refuses to start without it. | | `STACKS_ROOT` | no | `/home/shcizo/self-hosted` | Required parent for any stack eligible to update. | | `PORT` | no | `8080` | HTTP listen port. | | `LOG_LEVEL` | no | `info` | `debug` / `info` / `warn` / `error`. | | `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"`. | ## Endpoints | Endpoint | Auth | Purpose | |---|---|---| | `POST /update` | Bearer token | Trigger pull + restart for matching services | | `GET /healthz` | none | Liveness + Docker socket reachability | | `GET /version` | none | Build info | | `GET /metrics` | none | Prometheus exposition | `/healthz`, `/version`, and `/metrics` are intentionally unauthenticated — they're internal-network only behind the reverse proxy. ## Observability - **Logs**: JSON to stdout, picked up by Promtail/Alloy → Loki. - **Metrics**: Prometheus exposition on `/metrics`. Notable: `package_updater_update_jobs_total{project,service,status}`, `package_updater_last_update_timestamp{project,service}`, `package_updater_docker_ping_up`. ## Development ```bash go test ./... go build ./cmd/server docker build -t package-updater:dev . ``` ## Known v1 gaps These are tracked in the spec's section 2 and section 15 as deliberate out-of-scope: - **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. - **Single host only**. - **No per-repo API keys**: a single shared bearer token is used.