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
| Aspect | Watchtower (Auto) | Diun (Notify) | Manual | Hybrid |
|---|---|---|---|---|
| Update speed | Minutes after release | You decide | Whenever you remember | Varies by tier |
| Effort required | Near zero | Low (act on notifications) | High | Medium |
| Risk of breakage | Higher | Low (you review first) | Lowest | Balanced |
| Missed updates | None | Possible (ignore fatigue) | Likely | Unlikely |
| Best for | Stateless services | Databases, critical apps | Production | Most homelabs |
| Rollback plan | Needs preparation | Built into workflow | Built into workflow | Per-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:
- Filter aggressively — only watch containers you actually care about updating
- Use tag regex — watch
4.xpatterns instead of every single tag - Separate channels — security-critical notifications go to a high-priority channel, nice-to-haves go elsewhere
- 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:
- Check changelogs — skim release notes for breaking changes
- Backup volumes —
docker run --rm -v myapp_data:/data -v $(pwd):/backup alpine tar czf /backup/myapp-$(date +%Y%m%d).tar.gz /data - Pull and recreate —
docker compose pull && docker compose up -d - Verify — check the app’s web UI, logs (
docker compose logs --tail 50), and health endpoints - Clean up —
docker image prune -f - 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.
Option 4: The Hybrid Approach (Recommended)
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:
- Install both Watchtower and Diun
- Enable Watchtower (with
LABEL_ENABLE=true) for low-risk containers only - Enable Diun notifications for everything else
- Set a weekly calendar reminder for manual updates on critical services
- 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.