shcizo 574f425267 feat(web): service page, manual update, jobs list and live job log (#7)
Also publishes the jobs topic on agent revoke and updates the agent
snippet to mount stacks read-write at the same path.
2026-10-12 18:41:46 +02:00

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 and implementation plan 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.

Swarm mode

Set MODE=swarm to update Docker Swarm services instead of Compose stacks. The service runs docker service update --image via the Docker API instead of shelling out to docker compose.

Two things to get right when running in Swarm mode:

  • The opt-in label goes on the service, not the container/task. Swarm mode reads Service.Spec.Labels, so add it with docker service update --label-add se.shcizo.auto-update=true <service> or set it under deploy.labels in the stack file — a plain labels: entry on the service (container-level) is not visible to Swarm mode's discovery.
  • The updater must talk to a Swarm manager. docker service update requires manager API access, so either point DOCKER_HOST at a manager node or schedule the updater container itself on a manager with the socket mounted. This is a deployment concern the service cannot detect or work around.

Master/agent (in progress)

A master with one agent per machine is replacing the single-instance webhook model; see the design spec. Phase 1 delivers a live inventory: every Compose stack, Swarm service and container on every machine, behind Authentik login. Updates still go through the existing /update webhook until phase 3.

  • Master: updater-master in the same image, example stack in deploy/master.stack.yml. At startup it does OIDC discovery against OIDC_ISSUER and exits if the issuer is unreachable; Swarm then restarts it.
  • Agents: add one per machine on the master's Agents page; it shows a ready-to-run compose file with the agent's token and the master's key fingerprint.

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 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".
MODE no compose compose or swarm. Selects the update mechanism for the whole deployment; not mixed per-request.

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

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 in Compose mode. Swarm mode (MODE=swarm) is the multi-node path, but only from a manager node's point of view — the updater itself still needs manager API access (see "Swarm mode" above).
  • No per-repo API keys: a single shared bearer token is used.
S
Description
Simple app for updating local docker packages. Made to be used by a github/gitea action workflow
Readme
2.9 MiB
Languages
Go 99%
Dockerfile 1%