Generic auto-updater for docker compose stacks that deploy GHCR images with
immutable tags (e.g. sha-<commit> from docker/metadata-action). Watchtower-style
polling, but label-driven and pin-based: deployed tags live in the compose
.env file, so deployment state is explicit, auditable, and rollback is a
one-line edit.
Every POLL_INTERVAL seconds, for each labeled service:
- Query the GitHub Packages API for the newest tag matching
TAG_PATTERN - If it differs from the pin in
.env: pull → update pin →compose up -d <service> - If the restart fails, the pin is reverted and the next cycle retries
Services are discovered from the compose file itself — adding a service to the update rotation is just adding labels. No updater config changes.
1. Use the prebuilt image ghcr.io/dicodingacademy/watchdock:latest (published by
this repo's CI), or copy Dockerfile + updater.sh from the repo root into a
directory next to your compose file if you prefer building it yourself.
2. Pin a project name at the top of your compose file (required — without it, compose inside the container resolves a different project and will spawn a duplicate stack):
name: myproject3. Use variable tags + labels on each service you want auto-updated:
services:
myapp:
image: ghcr.io/myorg/myapp:${MYAPP_TAG:?set in .env}labels:
auto-update.tag-var: MYAPP_TAG # required: name of the pin var in .envauto-update.priority: "10"# optional: lower runs first (default 100)auto-update.tag-pattern: "^sha-[0-9a-f]{7,40}$"# optional: per-service tag regex overrideServices without the label are never touched. Dependency services that share
a tag var (e.g. a migration job using the same image) should NOT be labeled —
they get recreated automatically via depends_on when the labeled service updates.
4. Add the updater service:
updater:
image: ghcr.io/dicodingacademy/watchdock:latest# or, if you copied the files instead:# build: ./updaterenvironment:
GITHUB_TOKEN: ${GHCR_READ_TOKEN:?PAT with read:packages}POLL_INTERVAL: "300"# VERBOSE: "1" # log every check# TAG_PATTERN: "^sha-[0-9a-f]+$" # override tag regex# GHCR_USER: your-github-usernamevolumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./:/composerestart: unless-stoppedlogging:
driver: json-fileoptions:
max-size: "10m"max-file: "3"5. Seed .env with current tags + the token:
MYAPP_TAG=sha-abc1234
GHCR_READ_TOKEN=ghp_xxxx
6.docker compose up -d updater (add --build if you build it locally)
See .env.example for a ready-to-copy template.
| Env var | Default | Purpose |
|---|---|---|
GITHUB_TOKEN | (required) | PAT with read:packages; also used for GHCR pull |
GHCR_USER | token | Username for docker login ghcr.io |
COMPOSE_FILE | /compose/docker-compose.yml | Compose file path inside the container |
ENV_FILE | /compose/.env | Env file holding the tag pins |
POLL_INTERVAL | 300 | Seconds between cycles |
TAG_PATTERN | ^sha-[0-9a-f]{7,40}$ | Regex for deployable tags |
VERBOSE | 0 | 1 = log every check, even when up-to-date |
You can use these common regex patterns in TAG_PATTERN (global env var) or auto-update.tag-pattern (per-service label):
| Tag Format Example | Regex Pattern | Example Output |
|---|---|---|
sha-{commit-sha} (Default) | ^sha-[0-9a-f]{7,40}$ | sha-a1b2c3d |
{commit-sha} (Raw SHA) | ^[0-9a-f]{7,40}$ | a1b2c3d or a1b2c3d4e5f67890 |
| Tag Format Example | Regex Pattern | Example Output |
|---|---|---|
v{run_id}-{commit_sha} | ^v[0-9]+-[0-9a-f]{7,40}$ | v12345678-a1b2c3d |
{run_id}-{commit_sha} | ^[0-9]+-[0-9a-f]{7,40}$ | 12345678-a1b2c3d |
build-{number} / release-{number} | `^(build | release |
| Tag Format Example | Regex Pattern | Example Output |
|---|---|---|
v{major}.{minor}.{patch} (SemVer) | ^v?[0-9]+\.[0-9]+\.[0-9]+$ | v1.2.3 or 1.2.3 |
SemVer with Pre-release (rc, beta) | ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ | v1.2.3-rc.1 or 2.0.0-beta.2 |
YYYY.MM.DD / YYYY.MM.DD.patch (CalVer) | ^v?[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ | v2026.07.26 or 2026.07.26.1 |
YYYYMMDD-{commit_sha} (Date + SHA) | ^[0-9]{8}-[0-9a-f]{7,40}$ | 20260726-a1b2c3d |
| Tag Format Example | Regex Pattern | Example Output |
|---|---|---|
{branch}-{commit_sha} | `^(main | master |
{env}-{version} | `^(prod | staging |
Tip: You can combine multiple patterns using
|(OR), e.g.^(sha-[0-9a-f]{7,40}|v[0-9]+-[0-9a-f]{7,40})$.
Rollback: edit the tag var in .env to an older SHA, then
docker compose up -d <service>. The updater won't fight you unless a newer
version is published to GHCR.
Logs:docker compose logs -f updater. An hourly heartbeat is logged, so
silence longer than an hour means something is wrong.
Security note: mounting docker.sock grants this container effective root
on the host (standard for this pattern — Watchtower does the same). Keep the
PAT minimal-scope (read:packages only).
- GHCR images in an org (API path uses
/orgs/), private or public - Immutable tags matching
TAG_PATTERNpushed by CI - PAT with
read:packages(SSO-authorized if the org enforces SAML)