# Automatically Updating Docker Containers: Watchtower, Diun & Renovate Compared

6. May 2026

Missing notifications, GitLab chaos and the search for the perfect Docker update workflow

Watchtower sounds like the perfect solution for automatic Docker updates – until you realize you have no overview of what was updated when, and GitLab simply refuses to update properly. An honest experience report and what I do instead.

dark-homelab-server-rack-with-glowing-docker-wha

Image generated with AI

The idea is good. Really.

Anyone who runs a homelab knows the problem: at some point you end up with 15, 20, 30 Docker containers running. Each one needs to be maintained, each one occasionally gets security patches, new features, breaking changes. Manually typing docker pull and docker compose up -d on every stack is still fun at the beginning. After six months, not so much.

Watchtower promises the solution: A container that independently monitors all others, pulls new images and restarts the containers. Sounds perfect. I installed it, was briefly excited – and eventually turned it off again. Here's why.

# What Watchtower Does – and What It Promises

Watchtower runs as a Docker container and polls the registry for new image versions at configurable intervals. When it finds an update, it pulls the new image, stops the old container, and starts a new one with the same parameters. For simple, stateless containers, this sounds trivial – and it works just as well.

The text you provided is not a natural language text but rather a **Docker Compose configuration file** (YAML format). It does not require translation, as it is a technical/code configuration that is language-independent.

However, I can **explain the content in English**:

---

This is a **Docker Compose service definition** for **Watchtower**, an application that automatically updates Docker containers.

- **`image: containrrr/watchtower`** – Uses the official Watchtower Docker image.
- **`container_name: watchtower`** – Names the container "watchtower".
- **`restart: unless-stopped`** – The container restarts automatically unless manually stopped.
- **`volumes`** – Mounts the Docker socket to allow Watchtower to manage other containers.
- **`WATCHTOWER_CLEANUP=true`** – Removes old/unused images after updates.
- **`WATCHTOWER_SCHEDULE=0 0 4 * * *`** – Runs the update check daily at **4:00 AM**.
- **`WATCHTOWER_NOTIFICATION_URL`** – Sends notifications via **Gotify** (a self-hosted notification service) to the specified domain using a token.

---

The configuration itself remains **unchanged**, as code should not be translated.

Problem 1: Watchtower is practically unmaintained

Here is the translation: What many people don't know: The original containrrr/watchtower repository is considered unmaintained by large parts of the community. Newer Docker versions introduce API changes that the original image no longer understands.

# Typical error message with modern Docker versions:
Error response from daemon: client version 1.25 is too old.
Minimum supported API version is 1.40,
please upgrade your client to a newer version

The community has therefore split. One part uses workarounds, another has switched to the fork nicholas-fedor/watchtower – a 1:1 drop-in replacement that is actively maintained and supports current Docker APIs. However: The original Watchtower creator himself warned that many forks are "full of AI slop". Anyone who grants an unvetted fork access to their Docker socket is opening up a large attack surface.

Docker Socket = Full Host Access

Watchtower gets access to /var/run/docker.sock. This means it can, in principle, start, stop, or manipulate any container on the host. Unverified forks should not be blindly trusted here.

Problem 2: No overview of what was updated when

Watchtower updated – and you don't know about it. No push notification, no summary, no dashboard. You log in two days later and wonder: Is that still running last week's version, or has Watchtower pulled something in the meantime?

Yes, Watchtower theoretically supports notifications via the Shoutrrr library (Gotify, Slack, Discord, email). But in practice, I never really got it to work cleanly. The configuration is more cumbersome than it needs to be.

# This is what Watchtower logs look like – when you actively look for them:
docker logs watchtower --tail 20

# time="2026-04-01T04:00:12Z" level=info msg="Found new image for /uptime-kuma"
# time="2026-04-01T04:00:18Z" level=info msg="Stopping /uptime-kuma"
# time="2026-04-01T04:00:21Z" level=info msg="Creating /uptime-kuma"
# time="2026-04-01T04:00:23Z" level=info msg="Removing old image sha256:abc123"

# The problem: You have to actively check.
# Nothing comes to you. You don't know what was updated.

Problem 3: GitLab and Watchtower are conceptually incompatible

GitLab is the prime example of why blind auto-updating is dangerous. gitlab/gitlab-ce:latest sounds harmless – but it isn't. GitLab has a strict upgrade path: you cannot jump directly from version X to version X+5, but must follow certain intermediate stops.

# Allowed GitLab upgrade path (example):
# 16.0 → 16.3 → 16.7 → 16.11 → 17.0 → 17.3 → 17.x

# What Watchtower does:
# 16.0 → 17.9 (latest) – in one go

# What happens then:
docker logs gitlab --tail 5
# FATAL: Database migration failed!
# ActiveRecord::IrreversibleMigration
# Please follow the upgrade path: https://docs.gitlab.com/ee/update/

Watchtower knows no upgrade path, no database migrations, no dependencies between components. It only knows: old image → new image → restart. For a setup consisting of gitlab-ce, Runner, Redis and PostgreSQL, this is actively dangerous.

GitLab Rule

Exclude GitLab instances from Watchtower by default and always update them manually following the official upgrade path. No exceptions.

# The Silent Defect Problem

:latest does not guarantee backward compatibility. Other typical scenarios:

  • PostgreSQL Major Upgrade: Going from 15 to 16 requires pg_upgrade. Watchtower simply restarts the container – the database no longer boots.
  • Immich + Redis: Users explicitly warn against blindly updating Redis in an Immich stack – it breaks the entire application structure.
  • Configuration format changes: New image, different config format expected – container starts, but behaves incorrectly or not at all.

Here is the translation: "The insidious part: Often the container appears to start without any problems. The error only manifests itself hours later."

# Using Watchtower Correctly: Opt-In Instead of Opt-Out

If you use Watchtower, then use it with the label filter in opt-in mode. Only containers that are explicitly marked will be touched:

Here is the translation of the text into English:

services:
  watchtower:
    image: nicholas-fedor/watchtower   # Actively maintained fork
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 3 * * *
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_NOTIFICATION_URL=gotify://gotify.mydomain.com/TOKEN
    command: --label-enable

  # Will be updated automatically
  uptime-kuma:
    image: louislam/uptime-kuma:latest
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

  # Will NOT be touched
  gitlab:
    image: gitlab/gitlab-ce:17.0.0-ce.0   # Pinned!
    labels:
      - "com.centurylinklabs.watchtower.enable=false"

  # Will NOT be touched
  postgres:
    image: postgres:16.2                   # Also pinned!
    labels:
      - "com.centurylinklabs.watchtower.enable=false"

# GitLab CI + Watchtower: The API Trigger Approach

For self-developed services that are built via GitLab CI, there is a more elegant approach than periodic polling: the HTTP API trigger. Watchtower passively waits for a webhook – which the pipeline fires after a successful image push. No blind polling, no random restart timing.

# Watchtower in API mode
services:
  watchtower:
    image: nicholas-fedor/watchtower
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"   # Local only! Never expose directly to the network.
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_HTTP_API_UPDATE=true
      - WATCHTOWER_HTTP_API_TOKEN=${WATCHTOWER_TOKEN}
      - WATCHTOWER_CLEANUP=true
    command: --http-api-update
Here is the translation of the text into English:

# .gitlab-ci.yml – Deploy stage triggers Watchtower
deploy:
  stage: deploy
  image: curlimages/curl
  script:
    - |
      curl -s \
        -H "Authorization: Bearer $WATCHTOWER_TOKEN" \
        "https://myserver.com/v1/update?container=my-app"
  only:
    - main

**Notes on the translation:**
- `meinserver.de` was translated to `myserver.com` (German → English equivalent)
- `meine-app` was translated to `my-app` (German → English equivalent)
- All technical keywords, commands, and syntax remain unchanged, as they are part of the code and configuration

For accessing the GitLab Container Registry, Watchtower requires credentials. The most secure solution: a Deploy Token with read_registry scope – decoupled from user accounts, long-lived, minimal permissions.

# Docker config.json for GitLab Registry Auth
mkdir -p ~/.docker
echo '{
  "auths": {
    "registry.gitlab.com": {
      "auth": "'$(echo -n 'deploy-token:deploy-token-password' | base64)'"
    }
  }
}' > ~/.docker/config.json

# Mount in compose:
# volumes:
#   - /root/.docker/config.json:/config.json:ro
# environment:
#   - DOCKER_CONFIG=/config.json

Here is the translation: **The better alternative: Diun**

Diun (Docker Image Update Notifier) is a lightweight tool that does exactly one thing: it sends you a notification when a new image is available – and restarts nothing in the process. You decide what happens.

Here is the translation: Diun also runs as a Docker container, monitors all or selected running containers, and reports via configurable channels (Gotify, Telegram, Slack, email, ntfy, and many more). No silent updating, no surprises – just information.

Here is the translation of the text into English:

services:
  diun:
    image: crazymax/diun:latest
    container_name: diun
    restart: unless-stopped
    volumes:
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - TZ=Europe/Berlin
      - DIUN_WATCH_WORKERS=20
      - DIUN_WATCH_SCHEDULE=0 8 * * 1
      - DIUN_PROVIDERS_DOCKER=true
      - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true
      - DIUN_NOTIF_GOTIFY_ENDPOINT=https://gotify.mydomain.com
      - DIUN_NOTIF_GOTIFY_TOKEN=${GOTIFY_TOKEN}
      - DIUN_NOTIF_GOTIFY_PRIORITY=5

**Note:** The only change made was translating the German domain example `gotify.meinedomain.de` to the English equivalent `gotify.mydomain.com`. All other parts of the text are configuration code and remain unchanged, as they are not language-specific.

Here is the translation: Every Monday morning, a summary arrives showing which images have updates. I briefly check the changelog, decide whether to update now or wait – and then perform the update manually. Not fully automated, but I always know what's running on my server.

# The GitOps Approach: Renovate Bot

Renovate is a dependency update bot originally developed for npm, Maven and the like – but which now also understands docker-compose.yml files. For those who manage their Docker stacks in Git, this provides a completely different approach:

Renovate scans the repository, detects outdated image tags, and automatically opens a merge request in GitLab with the version update. You review the MR, take a quick look at the changelog, merge – and the CI/CD pipeline deploys. Every update is documented as a commit in the version history. No container is ever modified without your knowledge.

Here is the translation of the text into English:

// renovate.json – Configuration in the repository root
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "docker-compose": {
    "enabled": true
  },
  "packageRules": [
    {
      "matchPackageNames": ["postgres", "redis", "mariadb"],
      "matchUpdateTypes": ["major"],
      "enabled": false
    }
  ]
}

**Note:** The code itself remains unchanged, as code is language-independent. Only the comment at the beginning (`// renovate.json – Konfiguration im Repository-Root`) was translated into English (`// renovate.json – Configuration in the repository root`).

Renovate can be run on a self-hosted GitLab runner or used as a hosted service. The catch: it requires some effort during setup and a functioning CI/CD pipeline. For a hobby homelab it might be overkill – but for a production server it's the cleanest solution there is.

**Alternatives at a Glance**

  • Watchtower – Fully automatic, controllable with label filters. Original image barely maintained anymore.
  • nicholas-fedor/watchtower – Actively maintained fork, drop-in replacement with modern Docker API support.
  • Diun – Notifications only, no automatic updates. Best control, minimal risk.
  • WUD (What's Up Docker) – Web interface, visually displays outdated stacks, supports SemVer logic.
  • Renovate Bot – GitOps approach via merge requests. Highest traceability, requires CI/CD.
  • Podman Auto-Update – Native systemd timer integration. Requires migration from Docker to Podman.

# What I Actually Wish For

The desired solution in my head:

  1. Diun runs on the server and sends a summary every Monday showing which images have updates.
  2. Here is the translation: For non-critical containers, Watchtower runs in opt-in mode at night – with Gotify notification.
  3. For self-developed services: GitLab CI pushes the image and triggers Watchtower via API call.
  4. GitLab, databases, everything stateful: manually, according to the changelog, on request.

Here is the translation: "I find the Renovate approach conceptually the cleanest – but the effort exceeds my current pain threshold for a hobby homelab. Perhaps that will be the next article."

Conclusion: No Silver Bullet

Here is the translation: Watchtower is not bad – it's just not the complete, carefree solution it promises to be. The original image is effectively unmaintained, the most secure fork requires trusting a third party, and the concept of "pull latest and restart" doesn't fit complex, stateful services like GitLab or databases.

Here is the translation: "The missing notifications were the main reason for me to stop. A tool that secretly modifies my server without informing me is not a tool I want."

My current approach: Diun for an overview, manual for updates, Watchtower in opt-in mode only for truly non-critical containers. Not a perfect solution, but an honest one.

"How do you do that?"

Fully automated with Watchtower and it's running smoothly? Renovate in a GitOps setup? Or mostly manual? Write it in the comments.

Share:


Post your comment

Required for comment verification