Automating Docker Updates: Watchtower vs Diun vs Manual Strategies

Your homelab is running smoothly. Twenty containers, all humming along. Then a CVE drops for one of your images, and you realize you haven’t updated anything in three months.

Docker containers don’t update themselves. Unlike desktop apps with auto-update prompts or Linux packages with apt upgrade, containers stay pinned to whatever image you pulled at deploy time. Without a strategy, your self-hosted stack slowly drifts into a graveyard of outdated software.

This guide compares the three main approaches — fully automatic updates with Watchtower, notification-only monitoring with Diun, and structured manual workflows — then shows you how to build a hybrid strategy that matches your risk tolerance.


The Update Problem

Traditional Docker workflows look like this:

docker compose pull
docker compose up -d
docker image prune -f

Simple enough for one project. But when you’re running Immich, Paperless-ngx, Vaultwarden, three databases, a reverse proxy, monitoring tools, and a media stack — that’s a lot of docker compose pull across a lot of directories.

The risks of not updating:

  • Security vulnerabilities — known CVEs in old images
  • Missing features — you’re stuck on old versions while upstream improves
  • Compatibility drift — the longer you wait, the harder the eventual upgrade
  • Data format changes — some apps require sequential migrations (skipping versions breaks things)

The risks of updating blindly:

  • Breaking changes — upstream pushes a major version that changes config formats
  • Database migrations — automatic restarts mid-migration can corrupt data
  • Dependency conflicts — one container’s update breaks another’s connectivity
  • Rollback complexity — if you didn’t snapshot the volume, good luck

The right strategy balances both sides.


Strategy Comparison at a Glance

AspectWatchtower (Auto)Diun (Notify)ManualHybrid
Update speedMinutes after releaseYou decideWhenever you rememberVaries by tier
Effort requiredNear zeroLow (act on notifications)HighMedium
Risk of breakageHigherLow (you review first)LowestBalanced
Missed updatesNonePossible (ignore fatigue)LikelyUnlikely
Best forStateless servicesDatabases, critical appsProductionMost homelabs
Rollback planNeeds preparationBuilt into workflowBuilt into workflowPer-tier

Option 1: Watchtower — Fully Automatic Updates

Watchtower monitors your running containers, detects when a newer image is available, pulls it, and restarts the container with the original configuration — all automatically.

Quick Setup

services:
  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *  # 4 AM daily
      - WATCHTOWER_NOTIFICATIONS=shoutrrr
      - WATCHTOWER_NOTIFICATION_URL=telegram://TOKEN@telegram?chats=CHAT_ID
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

When Watchtower Works Well

  • Stateless containers — reverse proxies, dashboards, notification services
  • Containers with stable update patterns — projects that follow semver and don’t break configs
  • Dev/testing environments — where breakage is acceptable
  • Single-purpose containers — one function, minimal dependencies

When Watchtower Causes Problems

  • Databases — PostgreSQL, MariaDB, and MongoDB major version updates require explicit migration steps. Watchtower will happily pull PostgreSQL 17 when you’re on 16, and your data directory becomes incompatible.
  • Apps with migration scripts — tools like Immich, Nextcloud, and GitLab run database migrations on startup. If Watchtower restarts them mid-migration or skips a version, you get corruption.
  • Pinned version requirements — some stacks need specific version combinations (e.g., Nextcloud + specific Redis version).

Making Watchtower Safer

Use labels to control which containers Watchtower manages:

services:
  watchtower:
    image: containrrr/watchtower
    environment:
      - WATCHTOWER_LABEL_ENABLE=true  # Only update labeled containers
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  caddy:
    image: caddy:latest
    labels:
      - com.centurylinklabs.watchtower.enable=true  # Auto-update this one

  postgres:
    image: postgres:16  # Pinned version, no Watchtower label

For the full setup guide, see our Watchtower tutorial.


Option 2: Diun — Notify, Don’t Touch

Diun (Docker Image Update Notifier) takes the opposite approach: it watches for new images and tells you about them, but never updates anything automatically.

Quick Setup

services:
  diun:
    image: crazymax/diun:latest
    container_name: diun
    restart: unless-stopped
    command: serve
    environment:
      - DIUN_WATCH_SCHEDULE=0 */6 * * *
      - DIUN_PROVIDERS_DOCKER=true
      - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true
      - DIUN_NOTIF_TELEGRAM_TOKEN=your-bot-token
      - DIUN_NOTIF_TELEGRAM_CHATIDS=your-chat-id
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - diun-data:/data

volumes:
  diun-data:

When Diun Works Well

  • Databases and stateful services — you want to know about updates, but apply them manually with proper backup/migration steps
  • Production-like environments — where uptime matters
  • Complex multi-container stacks — where updating one service requires updating others
  • Version-pinned images — Diun can watch for new tags matching a regex pattern

The Notification Fatigue Problem

Diun’s biggest risk isn’t technical — it’s psychological. When you get 15 update notifications a day, you start ignoring them. Three weeks later, that critical security fix is buried under a pile of unread alerts.

Combat notification fatigue:

  1. Filter aggressively — only watch containers you actually care about updating
  2. Use tag regex — watch 4.x patterns instead of every single tag
  3. Separate channels — security-critical notifications go to a high-priority channel, nice-to-haves go elsewhere
  4. Batch your updates — set a weekly “update window” and process all notifications at once

For the full setup guide, see our Diun tutorial.


Option 3: Manual Workflows That Actually Work

“Manual” doesn’t have to mean “whenever I remember.” A structured manual workflow can be more reliable than automation if you commit to it.

The Weekly Update Script

Create a script that handles the repetitive parts while keeping you in control:

#!/bin/bash
# ~/bin/update-check.sh — Check for updates across all compose projects
set -euo pipefail

COMPOSE_DIRS=(
  ~/docker/caddy
  ~/docker/immich
  ~/docker/paperless
  ~/docker/vaultwarden
  ~/docker/monitoring
)

echo "=== Docker Update Check — $(date) ==="
echo ""

for dir in "${COMPOSE_DIRS[@]}"; do
  if [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/compose.yml" ]; then
    echo "📦 Checking $(basename "$dir")..."
    cd "$dir"
    
    # Pull new images (download only, don't restart)
    docker compose pull --quiet 2>/dev/null
    
    # Check if any service has a newer image
    UPDATES=$(docker compose images --format json 2>/dev/null | \
      python3 -c "
import sys, json
for line in sys.stdin:
    img = json.loads(line)
    # Compare running container image ID vs pulled image ID
    print(f\"  {img.get('Service', 'unknown')}: {img.get('Repository', '')}:{img.get('Tag', 'latest')}\")
" 2>/dev/null || echo "  (check manually)")
    
    echo "$UPDATES"
    echo ""
  fi
done

echo "To apply updates:"
echo "  cd <project-dir> && docker compose up -d && docker image prune -f"

The Update Checklist

For each update session, follow this process:

  1. Check changelogs — skim release notes for breaking changes
  2. Backup volumesdocker run --rm -v myapp_data:/data -v $(pwd):/backup alpine tar czf /backup/myapp-$(date +%Y%m%d).tar.gz /data
  3. Pull and recreatedocker compose pull && docker compose up -d
  4. Verify — check the app’s web UI, logs (docker compose logs --tail 50), and health endpoints
  5. Clean updocker image prune -f
  6. Document — note what you updated and any issues

Scheduled Update Windows

Pick a recurring time for updates:

  • Weekly for active homelabs with 10+ containers
  • Biweekly for stable setups with mostly mature software
  • Monthly at minimum — any less and you’re accumulating risk

Add it to your calendar. Treat it like any other maintenance task.


Most homelabs benefit from combining all three approaches. The key is categorizing your containers by risk level:

Tier 1: Auto-Update with Watchtower

Low-risk, stateless containers where breakage is trivial to fix:

  • Reverse proxies (Caddy, Traefik, Nginx Proxy Manager)
  • Dashboards (Homepage, Homarr, Heimdall)
  • Notification services (ntfy, Gotify, Apprise)
  • Monitoring exporters (node-exporter, cAdvisor)
  • Utility containers (Watchtower itself, Diun, Dozzle)
  • Download clients (qBittorrent, SABnzbd)
labels:
  - com.centurylinklabs.watchtower.enable=true

Tier 2: Notify with Diun

Important services where you want to review before updating:

  • Media servers (Jellyfin, Plex, Immich)
  • Document management (Paperless-ngx, Docuseal)
  • Productivity tools (Vikunja, Bookstack, Mealie)
  • Git hosting (Gitea, Gitness)
  • CI/CD (Woodpecker CI)
  • Chat/communication (Mattermost, Matrix)
labels:
  - diun.enable=true
  - com.centurylinklabs.watchtower.enable=false

Tier 3: Manual Only

Critical infrastructure where updates need planning:

  • Databases — PostgreSQL, MariaDB, MongoDB, Redis (major versions)
  • Authentication — Authentik, Authelia, Keycloak
  • Backup infrastructure — Kopia, Duplicati, Restic
  • Password managers — Vaultwarden
  • Encryption/VPN — WireGuard, Gluetun
labels:
  - diun.enable=true   # Still get notifications
  - com.centurylinklabs.watchtower.enable=false
  # But update manually with full backup + migration steps

Example Hybrid Docker Compose

services:
  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    environment:
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *
      - WATCHTOWER_NOTIFICATIONS=shoutrrr
      - WATCHTOWER_NOTIFICATION_URL=telegram://TOKEN@telegram?chats=CHAT_ID
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  diun:
    image: crazymax/diun:latest
    container_name: diun
    restart: unless-stopped
    command: serve
    environment:
      - DIUN_WATCH_SCHEDULE=0 */6 * * *
      - DIUN_PROVIDERS_DOCKER=true
      - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=false
      - DIUN_NOTIF_TELEGRAM_TOKEN=your-bot-token
      - DIUN_NOTIF_TELEGRAM_CHATIDS=your-chat-id
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - diun-data:/data
    labels:
      - com.centurylinklabs.watchtower.enable=true  # Auto-update Diun itself

volumes:
  diun-data:

Database Update Safety

Databases deserve special attention. Here’s a safe update procedure for PostgreSQL, the most common self-hosted database:

#!/bin/bash
# Safe PostgreSQL minor update (e.g., 16.2 → 16.3)
set -euo pipefail

PROJECT_DIR=~/docker/myapp
cd "$PROJECT_DIR"

# 1. Backup
echo "📦 Backing up database..."
docker compose exec -T postgres pg_dumpall -U postgres > "backup-$(date +%Y%m%d-%H%M).sql"

# 2. Pull new image
echo "⬇️  Pulling latest image..."
docker compose pull postgres

# 3. Recreate (minor versions are safe for in-place restart)
echo "🔄 Recreating container..."
docker compose up -d postgres

# 4. Verify
echo "✅ Checking database..."
sleep 5
docker compose exec postgres pg_isready -U postgres
echo "Database is healthy!"

For major version upgrades (e.g., PostgreSQL 16 → 17), use pg_upgrade or dump-and-restore. Never let Watchtower handle this.


Monitoring Your Update Health

Regardless of strategy, know the age of your running images:

#!/bin/bash
# Show how old each container's image is
echo "Container Image Age Report — $(date)"
echo "================================"

docker ps --format '{{.Names}}' | sort | while read name; do
  created=$(docker inspect "$name" --format '{{.Created}}' 2>/dev/null | cut -d'T' -f1)
  image=$(docker inspect "$name" --format '{{.Config.Image}}' 2>/dev/null)
  
  if [ -n "$created" ]; then
    age=$(( ($(date +%s) - $(date -d "$created" +%s)) / 86400 ))
    if [ "$age" -gt 90 ]; then
      echo "🔴 $name ($image): ${age} days old"
    elif [ "$age" -gt 30 ]; then
      echo "🟡 $name ($image): ${age} days old"
    else
      echo "🟢 $name ($image): ${age} days old"
    fi
  fi
done

Pair this with a monthly calendar reminder. Any container over 90 days old should be investigated.


Troubleshooting Common Update Issues

Container won’t start after update

# Check logs
docker compose logs --tail 100 service_name

# Roll back to previous image
docker compose down
# Edit compose file to pin the previous version tag
docker compose up -d

Database migration fails mid-update

# Restore from backup
docker compose down
docker volume rm myapp_db_data
docker volume create myapp_db_data
docker run --rm -v myapp_db_data:/data -v $(pwd):/backup alpine \
  tar xzf /backup/db-backup-latest.tar.gz -C /
docker compose up -d

Watchtower updated something it shouldn’t have

Add the opt-out label immediately:

labels:
  - com.centurylinklabs.watchtower.enable=false

Then roll back the specific container to the previous version by pinning the image tag.

Diun sends too many notifications

Disable watch-by-default and explicitly label only the containers you want to monitor:

environment:
  - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=false

Wrapping Up

There’s no single right answer for Docker update management. The best strategy matches your risk tolerance, your available time, and the criticality of your services.

Start here:

  1. Install both Watchtower and Diun
  2. Enable Watchtower (with LABEL_ENABLE=true) for low-risk containers only
  3. Enable Diun notifications for everything else
  4. Set a weekly calendar reminder for manual updates on critical services
  5. Keep a backup script ready for your databases

The goal isn’t zero maintenance — it’s predictable maintenance. Automate what’s safe, get notified about the rest, and never let critical infrastructure go unpatched because you forgot.

For related guides, check out our articles on Docker healthchecks and restart policies, Docker volume management, and backup strategies with Kopia or Duplicati.