Watchtower: Automatic Docker Container Updates Without the Hassle
Keeping Docker containers up to date is one of the more tedious parts of self-hosting. You have to check for new images, pull them, and restart containers — for every service you run. Watchtower automates this: it watches your running containers, checks for updated images on a schedule, and restarts containers when updates are available.
Photo by Rineshkumar Ghirao on Unsplash
Whether you want fully automatic updates or just scheduled notifications, Watchtower handles it with minimal configuration.
How Watchtower Works
Watchtower runs as a Docker container alongside your other services. On a configured schedule, it:
- Checks each running container's image against the registry (Docker Hub by default)
- Pulls any updated image layers
- Stops the old container
- Starts a new container with the updated image, preserving all the original flags, volumes, and environment variables
- Removes the old image (optionally)
The new container is started with the same docker run arguments as the original, so your data volumes and configurations stay intact.
Basic Setup
The simplest possible deployment — add to your docker-compose.yml:
services:
watchtower:
image: containrrr/watchtower:latest
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true # remove old images
- WATCHTOWER_SCHEDULE=0 0 4 * * * # run daily at 4am
- TZ=America/Los_Angeles
The Docker socket mount is required — Watchtower needs to talk to the Docker daemon to manage containers.
Run docker compose up -d watchtower and Watchtower will start checking for updates on the configured schedule.
Scheduling
Watchtower uses cron syntax for scheduling. The format is: seconds minutes hours day-of-month month day-of-week
environment:
# Daily at 4:00 AM
- WATCHTOWER_SCHEDULE=0 0 4 * * *
# Every 6 hours
- WATCHTOWER_SCHEDULE=0 0 */6 * * *
# Weekly on Sunday at 3:00 AM
- WATCHTOWER_SCHEDULE=0 0 3 * * 0
Alternatively, set a polling interval in seconds:
environment:
- WATCHTOWER_POLL_INTERVAL=86400 # 24 hours in seconds
The cron approach is more predictable and easier to reason about.
Selective Updates
By default, Watchtower monitors all running containers. You can control this two ways:
Exclude specific containers (allow most, block some):
# In the container you DON'T want auto-updated:
labels:
- "com.centurylinklabs.watchtower.enable=false"
Allowlist mode (only update explicitly labeled containers):
# Watchtower configuration:
environment:
- WATCHTOWER_LABEL_ENABLE=true
# In containers you DO want auto-updated:
labels:
- "com.centurylinklabs.watchtower.enable=true"
The allowlist approach is safer for production: you opt-in specific services rather than having everything update automatically.
Monitor-Only Mode
If you're not ready for automatic updates but want to know when updates are available, use monitor-only mode:
environment:
- WATCHTOWER_MONITOR_ONLY=true
- WATCHTOWER_NOTIFICATIONS=slack
- WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=https://hooks.slack.com/...
In this mode, Watchtower checks for updates and sends notifications but doesn't pull or restart anything. You get the awareness without the risk.
Notifications
Watchtower can send notifications via multiple channels when updates occur:
Email:
environment:
- WATCHTOWER_NOTIFICATIONS=email
- [email protected]
- [email protected]
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.yourdomain.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
- [email protected]
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=yourpassword
Slack/Discord (via webhook):
environment:
- WATCHTOWER_NOTIFICATIONS=slack
- WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=https://hooks.slack.com/services/...
- WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=watchtower
Gotify (if you self-host notifications):
environment:
- WATCHTOWER_NOTIFICATIONS=gotify
- WATCHTOWER_NOTIFICATION_GOTIFY_URL=https://gotify.yourdomain.com
- WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN=your-token
Run Once Mode
Sometimes you want to trigger an update check manually rather than on a schedule:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower \
--run-once \
--cleanup
This pulls and updates all containers immediately, then exits. Useful for applying updates before a scheduled maintenance window.
Private Registries
If your containers come from a private registry (your own Docker Hub repo, GitHub Container Registry, etc.):
environment:
- REPO_USER=your-username
- REPO_PASS=your-password-or-token
For multiple registries, use Docker credentials stored in ~/.docker/config.json:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /root/.docker/config.json:/config.json
environment:
- DOCKER_CONFIG=/config.json
When to Use Automatic Updates
Good for automatic updates:
- Internal tools where brief downtime is acceptable
- Security-focused services (you want patches fast)
- Services with stable APIs that don't break between versions
Use monitor-only or manual instead:
- Database containers (version mismatches can corrupt data)
- Services with breaking changes between versions (check changelogs first)
- Production services where downtime or config changes have real consequences
A sensible default: use enable=true labels on your reverse proxy, monitoring tools, and utility containers. Leave databases and stateful services set to manual updates.
Complete Example
services:
watchtower:
image: containrrr/watchtower:latest
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 3 * * * # 3 AM daily
- WATCHTOWER_LABEL_ENABLE=true # allowlist mode
- WATCHTOWER_NOTIFICATIONS=email
- [email protected]
- [email protected]
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=mail.yourdomain.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=watchtower@yourdomain.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=yourpassword
- TZ=America/Los_Angeles
jellyfin:
image: jellyfin/jellyfin:latest
labels:
- "com.centurylinklabs.watchtower.enable=true" # auto-update this one
# ... rest of config
postgres:
image: postgres:16 # NOT labeled — manual updates
# ... rest of config
