Watchtower keeps your running Docker containers up to date: it watches for new images, pulls them, and recreates the container β€” then deletes the old image. It’s the quiet janitor behind a self-hosted stack.

This is the piece that acts on every other guide in Tools β€” each one carries a com.centurylinklabs.watchtower.enable=true label. Only labelled containers get touched (with --label-enable), so Watchtower updates exactly what you opt in.

The original is archived β€” use a maintained fork

containrrr/watchtower was archived on 2025-12-17 (read-only now, ~24k ⭐). The actively-maintained continuation is the Docker image nickfedor/watchtower (used below) β€” its source lives at nicholas-fedor/watchtower on GitHub (don’t be thrown by the name mismatch; it’s still shipping releases). It’s a drop-in replacement β€” same labels, same flags.

Tag discipline (the rule that keeps auto-updates safe)

Watchtower updates a container to the newest image for the tag you pinned. The tag is your blast radius:

  • postgres:14 β†’ updates to 14.1, 14.2, … (minor/patch) but not 15. βœ… Safe.
  • postgres:latest β†’ updates across major versions too (14 β†’ 15 β†’ 16). ⚠️ A major bump can break your app or need a migration β€” at 4am, unattended.

Pin to a major-version tag wherever the project versions cleanly. A note on the other Tools guides: several use :latest (Vaultwarden, Sshwifty) for convenience β€” fine while you’re reading release notes yourself, but under auto-update that means Watchtower will happily pull a breaking major. Either pin them, or put them in monitor-only (below).

Daily auto-updates β€” docker-compose.yml

# docker-compose file for watchtower (auto-updates labelled containers)
#   start:  docker compose up -d
name: watchtower-localhost
 
services:
  watchtower:
    image: nickfedor/watchtower
    container_name: watchtower.localhost
    restart: always
    mem_limit: 32M
 
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock   # gives it control of the daemon
      - /etc/localtime:/etc/localtime:ro            # so the schedule uses local time
 
    labels:
      - com.centurylinklabs.watchtower.enable=true  # update watchtower itself too
 
    # Update labelled containers at 04:15, clean up old images afterward.
    # (schedule is 6-field cron: sec min hour dom mon dow)
    command: --label-enable --cleanup --schedule "0 15 4 * * *"

The docker.sock mount is root-equivalent

Anything that can talk to /var/run/docker.sock effectively has root on the host. Watchtower needs it β€” so keep this container minimal, pinned, and don’t add anything else to it.

Update manually β€” run.sh

#!/bin/sh
# Run watchtower once against all labelled containers, then exit.
docker run --rm \
    -v /var/run/docker.sock:/var/run/docker.sock \
    nickfedor/watchtower --cleanup --label-enable --run-once

Suggested improvements

1. Turn on notifications β€” this is the big one. Auto-updates that silently break a service at 4am are the nightmare. Notifications tell you what updated so you can check it. Watchtower uses shoutrrr URLs β€” ntfy, Telegram, Discord, Slack, email:

    environment:
      - WATCHTOWER_NOTIFICATIONS=shoutrrr
      - WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my-watchtower-topic
      - WATCHTOWER_NOTIFICATION_REPORT=true   # only notify when something changed

2. Monitor-only for anything you can’t afford to break blindly. For a password manager or a database, notify but don’t auto-update β€” then you update by hand after reading the release notes. Per-container, no global change:

    labels:
      - com.centurylinklabs.watchtower.enable=true
      - com.centurylinklabs.watchtower.monitor-only=true   # check + notify, don't update

3. Rolling restart (--rolling-restart) updates one container at a time instead of all at once β€” gentler on a busy host.

4. Ubuntu + AppArmor gotcha (you hit this): AppArmor can stop Watchtower from shutting containers down. Fix with sudo aa-remove-unknown, then verify with a manual ./run.sh --run-once.

5. Watch the log after the first scheduled run β€” docker logs watchtower.localhost confirms it’s finding your labelled containers and not erroring on the socket.