Skip to content
SumGuy's Ramblings
Go back

Watchtower vs Diun: Automating Docker Updates Without Burning Your Stack

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:

  1. Watchtower polls your configured registries on a schedule
  2. It compares the digest of your running image against the latest available
  3. If there’s a newer version, it pulls it down
  4. It stops the old container and starts a new one with identical runtime options
  5. 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:

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

  1. Diun watches your running containers (or a configured list of images)
  2. It checks registries for newer versions on a schedule
  3. When it finds updates, it sends you a notification
  4. 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:

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:

LabelDescription
diun.enableEnable/disable watching this container
diun.watch_repoWatch all tags in the repository
diun.max_tagsMax number of tags to watch when watching repo
diun.include_tagsRegex to include specific tags
diun.exclude_tagsRegex to exclude specific tags
diun.sort_tagsHow to sort tags: default, reverse, semver, lexicographical
diun.platformPlatform 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

FeatureWatchtowerDiun
Auto-updates containersYes (default behavior)No (notification only)
Monitor-only modeYes (opt-in)Yes (it’s the only mode)
Notification servicesVia Shoutrrr (15+ services)Native support (12+ services)
Label filteringBasic (enable/disable)Advanced (tag regex, semver sorting)
Private registry authDocker config.jsonBuilt-in regopts + Docker config
Watch non-running imagesNoYes
Custom notification templatesLimitedFull Go template support
Resource usageLowVery low
Configuration complexitySimpleModerate
Docker Compose supportYesYes
Kubernetes supportNoYes (via provider)
Cron schedulingYesYes
Cleanup old imagesYesN/A (doesn’t pull images)

So Which One Should You Use?

Here’s my honest take.

Use Watchtower If:

Use Diun If:

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)

  1. 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.

  2. Pin your image versions. Use nginx:1.25 instead of nginx:latest. This gives you predictable behavior even with auto-updates, because you’ll only get patch releases.

  3. 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.

  4. Back up before major updates. This should be automatic and unrelated to your update strategy, but it’s worth repeating.

  5. 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.

  6. Use Docker Compose restart policies. Both tools work best when your containers have restart: unless-stopped or restart: always set. This ensures containers come back up after updates.

  7. 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.


Share this post on:

Previous Post
Your First Open Source Contribution: Less Scary Than You Think, More Useful Than You Know
Next Post
Uptime Kuma: Status Pages, Alerts, and Knowing Before Your Users Do