Skip to content

Skiley/trigger-docker-pull

Repository files navigation

trigger-docker-pull

Tiny HTTP service that pulls a new Docker image and recreates a container on demand.

Push-based alternative to Watchtower: instead of polling a registry every N seconds, your CI calls a webhook the moment the new image is pushed, and the container is updated within seconds — with zero idle bandwidth.

What it does

POST /notify/{container_name} — for each request:

  1. Inspects the running container with that name.
  2. Pulls the image it's configured to use (same registry, same tag).
  3. Compares the resulting image digest to the running container's digest.
  4. If it changed: stops the container, removes it, recreates it from the same config + new image, reconnects networks, starts it.
  5. If it didn't change: logs already up to date and does nothing.

Failures are logged and swallowed — the next webhook gets a fresh attempt. There is no retry, no rollback, no state.

Why push instead of poll

  • Faster. Watchtower's default poll interval is 60s+; webhooks land in <1s.
  • Cheaper. No constant HEAD requests against your registry.
  • Targeted. Only the container that actually changed is touched. No accidental restarts of unrelated services that happen to share a label.

How requests are processed

  • Requests are accepted immediately (returns 202 Accepted) and enqueued in an in-memory bounded channel.
  • A single worker drains the queue. Containers are never processed in parallel — 500 concurrent webhooks won't tear down 500 containers at once.
  • Duplicate requests for the same container that arrive in the same batch are coalesced into a single update.
  • If the queue fills (default capacity: 1024), excess requests return 503 Service Unavailable. They are not persisted across restarts.

Endpoints

Method Path Response
POST /notify/{container_name} 202 queued · 401 missing/bad secret · 503 queue full · 400 empty name
GET /health 200 ok (unauthenticated)

/notify requires Authorization: Bearer <secret> where <secret> matches the SECRET_KEY env var. Requests with a missing, malformed, or non-matching header return 401 instantly and are never enqueued. The secret is compared in constant time.

/health is unauthenticated so liveness/readiness probes don't need credentials.

Quick start

docker run -d \
  --name trigger-docker-pull \
  --restart unless-stopped \
  -e SECRET_KEY=$(openssl rand -hex 32) \
  -p 8080:8080 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  trigger-docker-pull:latest

Then, from your CI pipeline after pushing a new image:

curl -fsS -X POST \
  -H "Authorization: Bearer $SECRET_KEY" \
  https://your-host.example.com/notify/my-api

docker-compose

services:
	trigger-docker-pull:
		image: trigger-docker-pull:latest
		container_name: trigger-docker-pull
		environment:
			SECRET_KEY: ${SECRET_KEY}
		restart: unless-stopped
		ports:
			- "8080:8080"
		volumes:
			- /var/run/docker.sock:/var/run/docker.sock

Private registries

Mount a Docker config.json to authenticate pulls:

volumes:
	- /var/run/docker.sock:/var/run/docker.sock
	- ./docker-config.json:/root/.docker/config.json:ro

The file uses Docker's standard format:

{
	"auths": {
		"ghcr.io": {
			"auth": "base64(username:password_or_token)"
		}
	}
}

Both bare (ghcr.io) and prefixed (https://ghcr.io, https://index.docker.io/v1/) registry keys are supported. Override the path with DOCKER_CONFIG_PATH.

GitHub Actions example

-   name: Build and push image
    uses: docker/build-push-action@v6
    with:
		push: true
	    tags: ghcr.io/${{ github.repository }}:latest

-   name: Trigger deploy
    env:
		SECRET_KEY: ${{ secrets.PUSH_TRIGGER_SECRET_KEY }}
    run: |
		curl -fsS -X POST \
		  -H "Authorization: Bearer $SECRET_KEY" \
		  https://deploy.example.com/notify/my-api

Configuration

Variable Default Notes
SECRET_KEY required Bearer token required on every /notify request. Service refuses to start if unset or empty.
PORT 8080 TCP port the HTTP server binds to.
QUEUE_SIZE 1024 In-memory queue capacity. Excess returns 503.
DOCKER_CONFIG_PATH /root/.docker/config.json Where to read registry credentials from.
RUST_LOG info Log level. Standard tracing filter syntax.

How the recreate works

The service reads the container's full inspect response, then on update:

  1. docker stop (10s grace) — best-effort, errors ignored.
  2. docker rm --force.
  3. docker create with the same config, host config, image (new), and labels.
  4. Reattaches each user-defined network the old container was on, preserving aliases.
  5. docker start.

This is similar to what Watchtower does — there is no in-place "update image" Docker API, only stop/remove/create.

Caveats

  • Brief downtime. Stop → remove → create → start takes a few seconds depending on the image. There is no zero-downtime rolling update; that's a job for an orchestrator.
  • State. If the container had ephemeral state inside its writable layer (it shouldn't), it's gone after recreate. Use volumes for anything you want to keep.
  • Failure mode. If the new image fails to pull or the container fails to start, the old container is already gone. The service logs and moves on — your monitoring should catch a down container.

Building from source

cargo build --release
./target/release/trigger-docker-pull

Or build the container locally:

docker build -t trigger-docker-pull .

The release binary is statically linked (musl) and the runtime image is FROM scratch, so the final image is just the stripped binary — a few MB.

License

MIT — see LICENSE.

About

Tiny HTTP service that pulls a new Docker image and recreates a container on demand.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors