An equivalent solution to Portainer BE's Automatic Stack Updates feature, but free.
Just run the container, tell it how to access your portainer instance, and tada, it's done! 🎉
# docker-compose.yml
services:
stack-webhook:
image: aklinker1/portainer-stack-webhook
ports:
- 3000:3000
environment:
BASE_URL: https://portainer.example.com/api # Required, full URL including /api
PORT: 3000 # Optional, default 3000
POLL_INTERVAL_MS: 5000 # Optional, status poll interval, default 5s
POLL_TIMEOUT_MS: 1800000 # Optional, give up after this long, default 30m (0 = no timeout)
LOG_LEVEL: info # Optional, debug|info|warn|error|silent, default info
stop_grace_period: 35m # Optional, let in-flight redeploys finish on restartAuthentication is per-request: every request must include an X-API-Key header containing a Portainer API access token. The token is forwarded to your Portainer instance, so the webhook performs whatever actions that token is allowed to.
To tell Portainer to pull the latest images and update the stack, make a simple POST request:
curl -X POST \
-H "X-API-Key: your-portainer-api-token" \
http://localhost:3000/api/webhook/stacks/:stackIdPortainer redeploys stacks asynchronously, so the webhook waits for the redeploy to finish before responding (polling the stack status every POLL_INTERVAL_MS, up to POLL_TIMEOUT_MS):
200with{ "id", "name", "status" }once the stack is active again.502if the redeploy fails.504if it doesn't finish withinPOLL_TIMEOUT_MS.
Because pulling images can take many minutes (e.g. large or Windows images), the request can stay open for a long time. Make sure any client or reverse proxy in front of the webhook allows long-lived requests.
Note
The stackId can be retrieved from the URL when visiting the stack details in Portainer. In the URL below, it would be 22 from the id=22 query parameter.
https://portainer.example.com/#!/1/docker/stacks/some_stack?id=22&type=1®ular=true&external=false&orphaned=false
The service logs each request and the full redeploy lifecycle — trigger, update accepted, every status poll, and the final outcome — as colored, timestamped lines. Use LOG_LEVEL to control verbosity (debug adds per-request entry lines; warn hides the routine poll chatter). Colors are emitted only on a TTY and disabled when NO_COLOR is set, so aggregated logs stay plain text. The X-API-Key is never logged.
- Health check:
GET /healthreturns200 { "status": "ok", "version" }and requires no API key. The Docker image ships a built-inHEALTHCHECKthat polls it. - Graceful shutdown: on
SIGTERM/SIGINTthe server stops accepting new connections and waits for in-flight redeploys to finish. Because a redeploy can run for many minutes, set the container'sstop_grace_period(compose) /terminationGracePeriodSeconds(k8s) at least as high asPOLL_TIMEOUT_MS, or in-flight deploys will beSIGKILLed on restart.
To install dependencies:
bun installTo run:
- Copy the
.env.templateto.envand fill it out with your portainer instance's info:cp .env.template .env
- Start the server
bun dev
- Send a request to test it out
curl -X POST \ -H "X-API-Key: your-portainer-api-token" \ http://localhost:3000/api/webhook/stacks/123
You can also run tests:
bun test