Docker & DevOps

Update self-hosted containers safely with Watchtower

Watchtower pulls new images and restarts your containers on a schedule. This guide covers monitor-only runs, allowlists, notifications and settings that keep updates reversible.

IntermediateLinux server5 min read

Watchtower is a small container that watches the containers running on your Docker host and pulls a new image whenever the tag you deployed points to a new digest. When it finds one, it stops that container, starts it again on the new image, and moves on. This guide shows how to run Watchtower on a Linux VPS without letting it restart something you did not want touched: you will do a monitor-only pass, switch to a schedule, allowlist the containers it may update, and wire up notifications.

Before you start

  • A Linux VPS with Docker Engine and the Compose plugin. docker compose version should print a version.
  • Root, or a user in the docker group, because Watchtower needs to talk to the Docker socket.
  • Your services already running from a Compose file, ideally with images pinned to a specific tag rather than :latest.
  • Somewhere to send alerts: a Discord or Slack webhook, a Telegram bot, or an SMTP account.

Warning: Watchtower restarts containers. For stateful services such as MariaDB, PostgreSQL or Nextcloud, a restart during a write can interrupt users, and a major version jump can migrate data in a way you cannot undo. Pin those images to a fixed version tag and update them by hand after reading the release notes.

Check the current image name and flags in the project's own documentation before you copy anything below. The project has changed maintainers and image names over time, and flags do get renamed.

Decide what Watchtower may touch

Two label modes control this:

  • Allowlist (recommended). Start Watchtower with --label-enable. It then only updates containers carrying com.centurylinklabs.watchtower.enable=true.
  • Denylist. Without that flag, Watchtower updates everything except containers carrying com.centurylinklabs.watchtower.enable=false.

The allowlist is safer: a new service you add next month stays untouched until you opt it in.

Add the label to a service in your Compose file:

YAML
services:
  gitea:
    image: gitea/gitea:1.22
    labels:
      com.centurylinklabs.watchtower.enable: "true"

Run Watchtower once in monitor-only mode

Before you let anything restart, run a single pass that only reports what it would do. --monitor-only disables the update step, and --run-once exits after one cycle.

Bash
docker run --rm \
  --name watchtower-monitor \
  -v /var/run/docker.sock:/var/run/docker.sock \
  containrrr/watchtower \
  --monitor-only \
  --run-once \
  --label-enable \
  --cleanup

Read the output. You should see one line per container, either a session summary with nothing to do, or a note that a newer image exists. If a container you care about shows up as updatable and you do not want that, add the label before you continue.

Schedule updates with Docker Compose

Run Watchtower as a long-lived service next to your apps. Create watchtower-compose.yml:

YAML
services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      TZ: "UTC"
      WATCHTOWER_SCHEDULE: "0 0 4 * * *"
      WATCHTOWER_CLEANUP: "true"
      WATCHTOWER_LABEL_ENABLE: "true"
      WATCHTOWER_ROLLING_RESTART: "true"
      WATCHTOWER_TIMEOUT: "30s"
      WATCHTOWER_NOTIFICATION_URL: "discord://token@webhook-id"

What the settings do:

  • WATCHTOWER_SCHEDULE is a six-field cron expression, seconds first. 0 0 4 * * * is 04:00 every day.
  • WATCHTOWER_CLEANUP removes the old image after a successful update, which keeps disk usage down.
  • WATCHTOWER_ROLLING_RESTART restarts containers one at a time instead of all at once.
  • WATCHTOWER_TIMEOUT is how long Watchtower waits for a container to stop before killing it.

Start it and follow the log:

Bash
docker compose -f watchtower-compose.yml up -d
docker logs -f watchtower

Tip: If you want different schedules for different groups of containers, run two Watchtower instances and give each one a --scope value. Containers are matched to a scope with the com.centurylinklabs.watchtower.scope label.

Set up notifications

WATCHTOWER_NOTIFICATION_URL takes a shoutrrr URL. The scheme decides the destination:

discord://token@webhook-id
slack://token-a/token-b/token-c
telegram://bot-token@telegram/?channels=channel-name
smtp://user:password@mail.example.com:587/?fromAddress=watchtower@example.com&toAddresses=you@example.com

The exact query parameters differ per service, so copy the format from the shoutrrr documentation rather than guessing. After a change, restart the container and check docker logs watchtower for a line confirming the notification was sent.

Check it works

  1. Confirm Watchtower is running: docker inspect -f '{{.State.Status}}' watchtower should print running.
  2. Watch one cycle in the log: docker logs --since 10m watchtower. You want a session start and a session end line.
  3. Force a pass on a low-risk container by changing its image tag to a newer patch release, then wait for the next cycle or trigger one with docker exec watchtower /watchtower --run-once.
  4. Check the container came back: docker ps --filter name=gitea should show a recent uptime, and the app should answer on its URL.

Troubleshooting

  • A container is never updated. With --label-enable set, Watchtower ignores anything without com.centurylinklabs.watchtower.enable=true. Add the label and recreate the container.
  • The app returns 502 after an update. The container restarted but the app is still booting. Add a health check so the proxy waits for it, as described in Add health checks to Docker containers.
  • Disk fills up with old images. WATCHTOWER_CLEANUP is not set, or an update failed and the old image is still in use. Check docker image ls, then run docker image prune.
  • permission denied while connecting to the Docker socket. The user running Watchtower is not in the docker group. Either run it as root, or add the user with sudo usermod -aG docker deploy and log in again.
  • A database container updated and the data will not load. A major version bump ran a migration. Restore from your backup, pin the image to the previous version tag, and update it manually after reading the release notes.

Next steps

  • [Update Docker containers safely](/guides
Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.