The Problem: Your Containers Are Getting Stale
Here’s a fun thought experiment. Go check how old your running Docker images are right now. I’ll wait.
If you’re anything like me, at least three of them haven’t been updated since the last time you “meant to get around to it.” And that was… well, let’s not talk about that.
Running outdated containers isn’t just a style problem. Old images mean unpatched vulnerabilities, missed bug fixes, and the slow creep of technical debt that eventually bites you at the worst possible time — like when you’re trying to show someone your cool self-hosted setup and Jellyfin crashes because you’re running a version from the Mesozoic era.
The Docker ecosystem has two popular solutions for this: Watchtower and Diun. They approach the problem from completely different philosophies, and picking the wrong one for your setup is the difference between a smooth-running homelab and waking up to a stack that looks like it was hit by a truck.
Let’s break them both down.
Watchtower: The Eager Intern
Watchtower is the “just handle it” approach. You point it at your running containers, and it periodically checks Docker Hub (or whatever registry you’re using) for newer images. When it finds one, it pulls the new image, gracefully stops the old container, and starts a new one with the same configuration.
Think of Watchtower as that eager intern who reorganizes your entire desk while you’re at lunch. Sometimes that’s exactly what you want. Sometimes you come back and can’t find anything.
How Watchtower Works
The workflow is dead simple:
- Watchtower polls your configured registries on a schedule
- It compares the digest of your running image against the latest available
- If there’s a newer version, it pulls it down
- It stops the old container and starts a new one with identical runtime options
- Optionally, it cleans up the old image
No human intervention needed. That’s both the feature and the risk.
Basic Watchtower Docker Compose Setup
Here’s the minimal setup to get Watchtower watching everything:
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=86400
restart: unless-stopped
That WATCHTOWER_POLL_INTERVAL is in seconds. 86400 = once per day. You can also use a cron expression instead:
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
That checks for updates at 4 AM every day. Because if something’s going to break, it might as well break while you’re asleep and blissfully unaware.
Watchtower with Notifications
Running Watchtower without notifications is like driving with your eyes closed and hoping for the best. You need to know what it’s doing:
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=discord://token@webhookid
restart: unless-stopped
Watchtower uses Shoutrrr under the hood, which supports a ridiculous number of notification services:
- Discord:
discord://token@webhookid - Slack:
slack://hook:token-a/token-b/token-c - Email (SMTP):
smtp://user:password@host:port/?from=sender&to=recipient - Telegram:
telegram://token@telegram?channels=channel-1 - Gotify:
gotify://gotify-host/token - Ntfy:
ntfy://ntfy.sh/mytopic - Pushover:
pushover://shoutrrr:token@user
You can even stack multiple notification URLs by separating them with spaces. Want Discord AND email? Go for it:
- WATCHTOWER_NOTIFICATION_URL=discord://token@webhookid smtp://user:pass@mail.example.com:587/?from=watchtower@example.com&to=you@example.com
Label-Based Filtering in Watchtower
Here’s where it gets interesting. You probably don’t want Watchtower updating everything. Your database container? Maybe leave that alone. That custom app you built and tagged latest because you’re a monster? Definitely leave that alone.
Monitor Only Mode (Don’t Touch, Just Look)
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_MONITOR_ONLY=true
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=discord://token@webhookid
restart: unless-stopped
This turns Watchtower into a notification-only tool. It’ll check for updates and tell you about them, but it won’t actually update anything. Basically, it becomes a slightly more aggressive version of Diun (foreshadowing).
Label-Based Opt-In
Want Watchtower to only update specific containers? Use WATCHTOWER_LABEL_ENABLE:
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_LABEL_ENABLE=true
restart: unless-stopped
nginx:
image: nginx:latest
labels:
- "com.centurylinklabs.watchtower.enable=true"
postgres:
image: postgres:16
# No label = Watchtower won't touch it
Only containers with com.centurylinklabs.watchtower.enable=true get updated. Everything else is safe.
Label-Based Opt-Out
Or flip it around — update everything except specific containers:
postgres:
image: postgres:16
labels:
- "com.centurylinklabs.watchtower.enable=false"
This is the “update everything but please for the love of all that is holy leave my database alone” approach.
Diun: The Responsible Adult
Diun (Docker Image Update Notifier) takes a fundamentally different approach. It doesn’t update anything. Ever. It just watches for new images and tells you about them. What you do with that information is entirely your problem.
If Watchtower is the eager intern, Diun is the responsible coworker who sends you a polite Slack message saying “Hey, there’s a new version of that thing you’re running. Thought you should know.” And then they go back to their coffee.
How Diun Works
- Diun watches your running containers (or a configured list of images)
- It checks registries for newer versions on a schedule
- When it finds updates, it sends you a notification
- That’s it. That’s the whole thing.
No pulling. No stopping containers. No restarting. Just pure, unadulterated information.
Basic Diun Docker Compose Setup
services:
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- diun-data:/data
environment:
- TZ=America/New_York
- DIUN_WATCH_SCHEDULE=0 */6 * * *
- DIUN_WATCH_JITTER=30s
- DIUN_PROVIDERS_DOCKER=true
restart: unless-stopped
volumes:
diun-data:
That DIUN_WATCH_JITTER adds a random delay (up to 30 seconds) before each check. It’s a nice touch that prevents hammering registries if you’re running multiple instances. Diun is polite like that.
Diun with Discord Notifications
services:
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- diun-data:/data
environment:
- TZ=America/New_York
- DIUN_WATCH_SCHEDULE=0 */6 * * *
- DIUN_WATCH_JITTER=30s
- DIUN_PROVIDERS_DOCKER=true
- DIUN_NOTIF_DISCORD_WEBHOOKURL=https://discord.com/api/webhooks/xxxx/yyyy
- DIUN_NOTIF_DISCORD_MENTIONS=@everyone
restart: unless-stopped
volumes:
diun-data:
Diun Notification Options
Diun has its own notification system that’s impressively thorough:
- Discord: Webhook URL with optional role/user mentions
- Slack: Incoming webhook
- Telegram: Bot token + chat ID
- Email (SMTP): Full SMTP configuration
- Gotify: Server URL + token
- Ntfy: Topic-based notifications
- Matrix: For the privacy-conscious crowd
- Pushover: Push notifications to your phone
- Rocket.Chat: If that’s your thing
- Script: Run any custom script on notification (this is wildly powerful)
- Webhook: Hit any arbitrary URL with a JSON payload
That last one — webhook — is particularly interesting. You can pipe Diun notifications into literally anything: Home Assistant, n8n, Node-RED, a custom API, whatever. The world is your notification oyster.
Diun with Slack Notifications
environment:
- DIUN_NOTIF_SLACK_WEBHOOKURL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
Diun with Email Notifications
environment:
- DIUN_NOTIF_MAIL_HOST=smtp.gmail.com
- DIUN_NOTIF_MAIL_PORT=587
- DIUN_NOTIF_MAIL_SSL=false
- DIUN_NOTIF_MAIL_STARTTLS=true
- DIUN_NOTIF_MAIL_USERNAME=your-email@gmail.com
- DIUN_NOTIF_MAIL_PASSWORD=your-app-password
- DIUN_NOTIF_MAIL_FROM=your-email@gmail.com
- DIUN_NOTIF_MAIL_TO=your-email@gmail.com
Diun Label-Based Filtering
Like Watchtower, Diun supports labels for fine-grained control. But Diun’s labels are even more powerful because they can control which tags to watch:
nginx:
image: nginx:1.25
labels:
- "diun.enable=true"
- "diun.watch_repo=true"
- "diun.max_tags=5"
- "diun.include_tags=^1\\.25\\."
- "diun.sort_tags=semver"
postgres:
image: postgres:16
labels:
- "diun.enable=true"
- "diun.include_tags=^16\\."
See what’s happening there? You can tell Diun to only notify you about patch versions within your current major/minor release. No “hey, Postgres 17 is out!” when you’re on 16 and not ready to deal with that kind of life event.
The available labels include:
| Label | Description |
|---|---|
diun.enable | Enable/disable watching this container |
diun.watch_repo | Watch all tags in the repository |
diun.max_tags | Max number of tags to watch when watching repo |
diun.include_tags | Regex to include specific tags |
diun.exclude_tags | Regex to exclude specific tags |
diun.sort_tags | How to sort tags: default, reverse, semver, lexicographical |
diun.platform | Platform to watch (e.g., linux/amd64) |
diun.metadata.* | Custom metadata to include in notifications |
Diun Configuration File (Advanced)
For more complex setups, Diun supports a YAML configuration file. This is useful when you want to watch images that aren’t currently running:
# diun.yml
watch:
schedule: "0 */6 * * *"
jitter: 30s
firstCheckNotif: false
providers:
docker:
watchByDefault: false
watchStopped: true
notif:
discord:
webhookURL: https://discord.com/api/webhooks/xxxx/yyyy
mentions:
- "@role:DevOps"
renderFields: true
templateBody: |
Docker tag {{ .Entry.Image }} which you subscribed to through {{ .Entry.Provider }} provider has been {{ if (eq .Entry.Status "new") }}newly added{{ else }}updated{{ end }}.
regopts:
- name: "ghcr"
selector: "ghcr.io"
username: your-github-username
password: your-github-pat
Mount it like so:
services:
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- diun-data:/data
- ./diun.yml:/diun.yml:ro
environment:
- TZ=America/New_York
- CONFIG=/diun.yml
restart: unless-stopped
Notice the regopts section? That’s how you authenticate to private registries like GHCR, GitLab Container Registry, or your own self-hosted registry. Both tools support private registries, but Diun makes the configuration particularly clean.
The Risks of Auto-Updating (a.k.a. Why Watchtower Can Hurt You)
Let’s get real for a second. Auto-updating Docker containers is playing with fire. Beautiful, convenient fire, but fire nonetheless. Here’s what can go wrong:
Breaking Changes
Image maintainers are human. Sometimes latest gets a breaking change that nukes your configuration. If Watchtower pulls that update at 4 AM, you wake up to a broken service and no idea what changed.
Real example: Traefik v1 to v2 was a complete configuration overhaul. If you were running traefik:latest with Watchtower enabled, congratulations — your reverse proxy just stopped working and every service behind it went dark.
Database Migrations Gone Wrong
Auto-updating database containers is particularly terrifying. A new Postgres version might require a manual migration step. Watchtower doesn’t know or care about that. It’ll happily pull the new image and watch your database refuse to start because the data directory is incompatible.
Dependency Chains
Your containers don’t live in isolation. App A depends on Database B depends on Cache C. Watchtower might update them in an order that breaks the chain. Or update one but not the others, creating a version mismatch that manifests as weird, hard-to-debug behavior.
The “Latest” Tag Problem
If you’re using the latest tag (no judgment — okay, a little judgment), you have zero control over what version you’re actually running. latest is a moving target. Combine that with auto-updates and you’ve essentially given the image maintainer a root shell on your server that they can change at any time.
Resource Spikes
Pulling new images takes bandwidth and disk space. If Watchtower updates ten containers simultaneously, that’s a lot of image pulls happening at once. On a Raspberry Pi or a VPS with limited resources, this can temporarily tank performance for everything else.
Head-to-Head Comparison
| Feature | Watchtower | Diun |
|---|---|---|
| Auto-updates containers | Yes (default behavior) | No (notification only) |
| Monitor-only mode | Yes (opt-in) | Yes (it’s the only mode) |
| Notification services | Via Shoutrrr (15+ services) | Native support (12+ services) |
| Label filtering | Basic (enable/disable) | Advanced (tag regex, semver sorting) |
| Private registry auth | Docker config.json | Built-in regopts + Docker config |
| Watch non-running images | No | Yes |
| Custom notification templates | Limited | Full Go template support |
| Resource usage | Low | Very low |
| Configuration complexity | Simple | Moderate |
| Docker Compose support | Yes | Yes |
| Kubernetes support | No | Yes (via provider) |
| Cron scheduling | Yes | Yes |
| Cleanup old images | Yes | N/A (doesn’t pull images) |
So Which One Should You Use?
Here’s my honest take.
Use Watchtower If:
- You’re running a homelab with services you can afford to have down briefly
- Your containers are all running pinned minor versions (e.g.,
nginx:1.25, notnginx:latest) - You have notifications configured so you know what changed
- You’re using label-based filtering to protect critical services
- You trust the image maintainers to not ship breaking changes in patch versions
- You value convenience over control
Use Diun If:
- You’re running anything remotely production-like
- You want to review updates before applying them
- You need to watch specific tag patterns (e.g., only patch releases)
- You’re running databases or stateful services that need careful upgrade paths
- You want to watch images you’re not currently running
- You value control over convenience
Use Both If:
Yeah, you can actually run both. Use Watchtower with WATCHTOWER_LABEL_ENABLE=true for low-risk containers (static sites, reverse proxies, media apps), and Diun for everything else. This gives you the best of both worlds:
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=discord://token@webhookid
restart: unless-stopped
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- diun-data:/data
environment:
- TZ=America/New_York
- DIUN_WATCH_SCHEDULE=0 */6 * * *
- DIUN_PROVIDERS_DOCKER=true
- DIUN_NOTIF_DISCORD_WEBHOOKURL=https://discord.com/api/webhooks/xxxx/yyyy
restart: unless-stopped
# Auto-updated by Watchtower
homepage:
image: ghcr.io/gethomepage/homepage:latest
labels:
- "com.centurylinklabs.watchtower.enable=true"
- "diun.enable=false"
# Monitored by Diun only
postgres:
image: postgres:16
labels:
- "com.centurylinklabs.watchtower.enable=false"
- "diun.enable=true"
- "diun.include_tags=^16\\."
volumes:
diun-data:
Best Practices (Regardless of Which You Pick)
-
Never auto-update databases. Just don’t. Postgres, MySQL, MariaDB, MongoDB — all of them need manual migration steps between major versions. Even minor versions can sometimes require attention.
-
Pin your image versions. Use
nginx:1.25instead ofnginx:latest. This gives you predictable behavior even with auto-updates, because you’ll only get patch releases. -
Always configure notifications. Whether you’re using Watchtower or Diun, you need to know what’s changing. Running either tool silently is asking for trouble.
-
Back up before major updates. This should be automatic and unrelated to your update strategy, but it’s worth repeating.
-
Test updates in a staging environment first. If you have the resources, maintain a separate stack where updates land first. Give it a day. If nothing catches fire, roll it out to production.
-
Use Docker Compose restart policies. Both tools work best when your containers have
restart: unless-stoppedorrestart: alwaysset. This ensures containers come back up after updates. -
Check changelogs. When Diun tells you there’s an update, actually read the release notes before applying it. That’s literally the whole point of using Diun over Watchtower.
Wrapping Up
There’s no universally correct answer here. Watchtower is a power tool — it does the work for you, but it can take your fingers off if you’re not careful. Diun is a monitoring tool — it keeps you informed and trusts you to make the right call.
For most homelabbers running a dozen or so containers, I’d actually recommend starting with Diun. Get comfortable with the notification workflow. Understand which of your images update frequently and which don’t. Learn which updates are safe to apply blindly and which need attention.
Then, once you have that knowledge, selectively add Watchtower for the containers you’re confident about. Label everything. Notify everything. And maybe don’t auto-update your database at 4 AM.
Your future self will thank you. Or at least won’t curse you. Which, in the world of self-hosting, is basically the same thing.