Watchtower is a small container that watches the containers running on your Docker host and pulls a new image whenever the tag you deployed points to a new digest. When it finds one, it stops that container, starts it again on the new image, and moves on. This guide shows how to run Watchtower on a Linux VPS without letting it restart something you did not want touched: you will do a monitor-only pass, switch to a schedule, allowlist the containers it may update, and wire up notifications.
Before you start
- A Linux VPS with Docker Engine and the Compose plugin.
docker compose versionshould print a version. - Root, or a user in the
dockergroup, because Watchtower needs to talk to the Docker socket. - Your services already running from a Compose file, ideally with images pinned to a specific tag rather than
:latest. - Somewhere to send alerts: a Discord or Slack webhook, a Telegram bot, or an SMTP account.
Warning: Watchtower restarts containers. For stateful services such as MariaDB, PostgreSQL or Nextcloud, a restart during a write can interrupt users, and a major version jump can migrate data in a way you cannot undo. Pin those images to a fixed version tag and update them by hand after reading the release notes.
Check the current image name and flags in the project's own documentation before you copy anything below. The project has changed maintainers and image names over time, and flags do get renamed.
Decide what Watchtower may touch
Two label modes control this:
- Allowlist (recommended). Start Watchtower with
--label-enable. It then only updates containers carryingcom.centurylinklabs.watchtower.enable=true. - Denylist. Without that flag, Watchtower updates everything except containers carrying
com.centurylinklabs.watchtower.enable=false.
The allowlist is safer: a new service you add next month stays untouched until you opt it in.
Add the label to a service in your Compose file:
services:
gitea:
image: gitea/gitea:1.22
labels:
com.centurylinklabs.watchtower.enable: "true"Run Watchtower once in monitor-only mode
Before you let anything restart, run a single pass that only reports what it would do. --monitor-only disables the update step, and --run-once exits after one cycle.
docker run --rm \
--name watchtower-monitor \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower \
--monitor-only \
--run-once \
--label-enable \
--cleanupRead the output. You should see one line per container, either a session summary with nothing to do, or a note that a newer image exists. If a container you care about shows up as updatable and you do not want that, add the label before you continue.
Schedule updates with Docker Compose
Run Watchtower as a long-lived service next to your apps. Create watchtower-compose.yml:
services:
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
TZ: "UTC"
WATCHTOWER_SCHEDULE: "0 0 4 * * *"
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_ROLLING_RESTART: "true"
WATCHTOWER_TIMEOUT: "30s"
WATCHTOWER_NOTIFICATION_URL: "discord://token@webhook-id"What the settings do:
WATCHTOWER_SCHEDULEis a six-field cron expression, seconds first.0 0 4 * * *is 04:00 every day.WATCHTOWER_CLEANUPremoves the old image after a successful update, which keeps disk usage down.WATCHTOWER_ROLLING_RESTARTrestarts containers one at a time instead of all at once.WATCHTOWER_TIMEOUTis how long Watchtower waits for a container to stop before killing it.
Start it and follow the log:
docker compose -f watchtower-compose.yml up -d
docker logs -f watchtowerTip: If you want different schedules for different groups of containers, run two Watchtower instances and give each one a --scope value. Containers are matched to a scope with the com.centurylinklabs.watchtower.scope label.
Set up notifications
WATCHTOWER_NOTIFICATION_URL takes a shoutrrr URL. The scheme decides the destination:
discord://token@webhook-id
slack://token-a/token-b/token-c
telegram://bot-token@telegram/?channels=channel-name
smtp://user:password@mail.example.com:587/?fromAddress=watchtower@example.com&toAddresses=you@example.comThe exact query parameters differ per service, so copy the format from the shoutrrr documentation rather than guessing. After a change, restart the container and check docker logs watchtower for a line confirming the notification was sent.
Check it works
- Confirm Watchtower is running:
docker inspect -f '{{.State.Status}}' watchtowershould printrunning. - Watch one cycle in the log:
docker logs --since 10m watchtower. You want a session start and a session end line. - Force a pass on a low-risk container by changing its image tag to a newer patch release, then wait for the next cycle or trigger one with
docker exec watchtower /watchtower --run-once. - Check the container came back:
docker ps --filter name=giteashould show a recent uptime, and the app should answer on its URL.
Troubleshooting
- A container is never updated. With
--label-enableset, Watchtower ignores anything withoutcom.centurylinklabs.watchtower.enable=true. Add the label and recreate the container. - The app returns 502 after an update. The container restarted but the app is still booting. Add a health check so the proxy waits for it, as described in Add health checks to Docker containers.
- Disk fills up with old images.
WATCHTOWER_CLEANUPis not set, or an update failed and the old image is still in use. Checkdocker image ls, then rundocker image prune. permission deniedwhile connecting to the Docker socket. The user running Watchtower is not in thedockergroup. Either run it as root, or add the user withsudo usermod -aG docker deployand log in again.- A database container updated and the data will not load. A major version bump ran a migration. Restore from your backup, pin the image to the previous version tag, and update it manually after reading the release notes.
Next steps
- [Update Docker containers safely](/guides
