One of the hidden maintenance costs of self-hosting is keeping everything updated. When you’re running 10, 20, or 50 Docker containers, manually pulling new images and recreating containers gets old fast. Miss an update and you might be running a version with known security vulnerabilities.
Watchtower solves this by automatically monitoring your running containers, pulling new images when they’re available, and gracefully restarting containers with the same configuration. Set it up once, and your homelab stays current without you lifting a finger.
What Is Watchtower?
Watchtower is a lightweight Docker container that watches your other containers. When it detects that a new image has been pushed to the registry, it pulls the image and restarts the container using the original docker run options or compose configuration.
Key features:
- Automatic updates on a configurable schedule
- Notifications via email, Slack, Discord, Telegram, Gotify, and more
- Selective monitoring — update only the containers you choose
- Cleanup — remove old images after updating
- Rolling restarts — minimize downtime
- Private registry support — works with any Docker registry
Prerequisites
- Docker and Docker Compose installed on your server
- Existing containers you want to keep updated
Step 1: Basic Setup
Create a directory and compose file:
mkdir -p ~/watchtower && cd ~/watchtower
Create docker-compose.yml:
services:
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: unless-stopped
environment:
- TZ=America/New_York
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
volumes:
- /var/run/docker.sock:/var/run/docker.sock
Start it:
docker compose up -d
That’s it for the basics. Watchtower will now check for updates every day at 4 AM and automatically update any container that has a newer image available.
Step 2: Understanding the Schedule
The WATCHTOWER_SCHEDULE uses 6-field cron syntax (with seconds):
Common schedules:
| Schedule | Cron Expression |
|---|---|
| Daily at 4 AM | 0 0 4 * * * |
| Every 6 hours | 0 0 */6 * * * |
| Sundays at 3 AM | 0 0 3 * * 0 |
| Every 30 minutes | 0 */30 * * * * |
Alternatively, use WATCHTOWER_POLL_INTERVAL for a simple interval in seconds:
- WATCHTOWER_POLL_INTERVAL=86400 # Every 24 hours
Tip: Schedule updates during low-traffic hours. Some updates require brief downtime during container restart.
Step 3: Set Up Notifications
Don’t update blindly — know what changed. Watchtower supports multiple notification channels.
Discord
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=discord://token@webhookid
Get the webhook URL from Discord (Server Settings → Integrations → Webhooks), then convert it:
Telegram
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=telegram://bottoken@telegram?channels=chatid
environment:
- WATCHTOWER_NOTIFICATIONS=email
- [email protected]
- [email protected]
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
- [email protected]
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app-password
Gotify (self-hosted)
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=gotify://gotify.example.com/tokenhere
Check the Shoutrrr docs for all supported services.
Step 4: Selective Updates (Monitor Mode vs Label Mode)
By default, Watchtower updates all running containers. This might not be what you want — some services (like databases) need careful upgrade planning.
Option A: Monitor Only (Don’t Auto-Update)
Set Watchtower to only notify, not update:
environment:
- WATCHTOWER_MONITOR_ONLY=true
This is great if you want awareness without automation.
Option B: Label-Based Control
Only update containers with a specific label:
environment:
- WATCHTOWER_LABEL_ENABLE=true
Then add the label to containers you want auto-updated:
# In your other docker-compose files:
services:
uptime-kuma:
image: louislam/uptime-kuma:latest
labels:
- "com.centurylinklabs.watchtower.enable=true"
Containers without the label are ignored.
Option C: Exclude Specific Containers
Keep auto-update on by default but exclude sensitive ones:
# On containers you DON'T want updated:
labels:
- "com.centurylinklabs.watchtower.enable=false"
Recommendation: Use label mode (WATCHTOWER_LABEL_ENABLE=true) and explicitly opt in. This gives you full control over what gets auto-updated.
Step 5: Handle Databases Safely
Databases like PostgreSQL, MySQL, and Redis should not be auto-updated. Major version upgrades often require migration steps that Watchtower can’t handle.
Always exclude databases:
services:
postgres:
image: postgres:16-alpine # Pin to major version
labels:
- "com.centurylinklabs.watchtower.enable=false"
For databases, pin to a major version (e.g., postgres:16-alpine not postgres:latest) and upgrade manually when ready.
Step 6: Private Registry Authentication
If you use private registries (GitHub Container Registry, GitLab, self-hosted), Watchtower needs credentials:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/.docker/config.json:/config.json:ro
Or set registry credentials via environment:
environment:
- REPO_USER=username
- REPO_PASS=password
Complete Production Setup
Here’s a full production-ready compose file combining everything:
services:
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
restart: unless-stopped
environment:
- TZ=America/New_York
# Schedule: daily at 4 AM
- WATCHTOWER_SCHEDULE=0 0 4 * * *
# Only update labeled containers
- WATCHTOWER_LABEL_ENABLE=true
# Clean up old images
- WATCHTOWER_CLEANUP=true
# Notifications via Discord
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=discord://token@webhookid
# Include stopped containers
- WATCHTOWER_INCLUDE_STOPPED=false
# Timeout for stopping containers gracefully
- WATCHTOWER_TIMEOUT=30s
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Resource limits
deploy:
resources:
limits:
memory: 128M
Checking Update Logs
View what Watchtower has been doing:
docker logs watchtower
You’ll see output like:
Troubleshooting
Watchtower isn’t updating containers
- Check the schedule is correct (remember: 6-field cron with seconds)
- If using label mode, verify containers have the watchtower label
- Check Docker socket permissions:
ls -la /var/run/docker.sock - View logs:
docker logs watchtower --tail 50
Container fails after update
Watchtower pulls the new image and recreates the container with the same config. If a new version introduces breaking changes:
# Roll back manually
docker stop container_name
docker rm container_name
docker run ... previous_image:tag # or update your compose file
This is why notifications matter — you’ll know immediately what was updated.
High memory usage during updates
Large image pulls can spike memory. The 128M limit in our production config prevents Watchtower from consuming too much. Increase if you see OOM kills.
Updates cause downtime
For zero-downtime updates on critical services, consider:
- Running redundant instances behind a load balancer
- Using Watchtower’s
--rolling-restartflag - Scheduling updates during maintenance windows
Alternatives to Watchtower
| Tool | Approach |
|---|---|
| Watchtower | Automatic pull + restart |
| Diun | Notification only (no auto-update) |
| Ouroboros | Similar to Watchtower (less maintained) |
| Renovate | Updates compose files in Git (GitOps approach) |
Watchtower is the most popular and actively maintained option for direct container updates.
Conclusion
Watchtower takes the most tedious part of self-hosting — keeping everything updated — and automates it completely. With label-based control and notifications, you get the convenience of auto-updates without losing control over sensitive services.
Start with monitor-only mode if you’re cautious, then gradually enable auto-updates for stable, well-tested containers. Your future self will thank you for not having to manually update 20 containers every week.
Related guides: