How to Deploy Watchtower for Docker Containers

How to Deploy Watchtower for Docker Containers

What You’ll Need

  • Hetzner VPS or Contabo VPS running Ubuntu or Debian
  • DigitalOcean as an alternative cloud hosting provider
  • Namecheap if you are setting up custom domains for reverse proxies
  • n8n Cloud or self-hosted n8n for receiving automated system alerts
  • Docker Engine version 20.10.0 or higher installed on your host system
  • Docker Compose version 2.0.0 or higher

Table of Contents

Understanding Watchtower for Docker Deployments

Managing production servers requires continuous maintenance, security patching, and image updates. When you run dozens of containerized services across cloud infrastructure, pulling new images, stopping containers, and recreating them manually quickly becomes unsustainable. Watchtower solves this problem by automating the entire lifecycle of container image updates.

Watchtower is a containerized application that monitors your running Docker containers. It regularly inspects the remote image registries where your images originated, checks if a newer tag or SHA hash exists, pulls the updated image, and gracefully restarts the container with its original runtime parameters.

I rely heavily on container automation when configuring lean infrastructure. If you want to optimize your resource usage across cheap servers, check out my guide on How I Run 3 Automated Systems on a Single $7/Month VPS.

Watchtower interacts directly with the host daemon by mounting the UNIX socket at /var/run/docker.sock. By reading the configuration state of every container, Watchtower captures port mappings, volume mounts, custom network definitions, environment variables, and restart policies. When a new image is pulled, Watchtower sends a graceful termination signal (SIGTERM) to the target container, waits for the stop timeout to expire, removes the old container instance, and instantiates a new container using the updated image alongside the exact initial configuration parameters.

Before deploying Watchtower, you must decide how frequently it should check registries and which containers it should be allowed to touch. Updating databases or stateful applications without a backup strategy can lead to database migration mismatches or data loss. In the following sections, I will walk you through setting up Watchtower securely with fine-grained control over updating schedules, labels, and notifications.

Deploying Watchtower via Docker Run and Docker Compose

To get started quickly on your server, provision a virtual machine on Hetzner VPS or Contabo VPS. You can run Watchtower as a standard Docker container using a single CLI execution.

Here is the fundamental CLI command to start Watchtower in standard monitoring and update mode:

docker run -d \
  --name watchtower \
  --restart always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  containrrr/watchtower \
  --cleanup \
  --interval 86400

In this command:

  • -v /var/run/docker.sock:/var/run/docker.sock grants Watchtower access to the Docker API daemon.
  • --cleanup automatically removes old image layers after updating containers to prevent your host disk from filling up.
  • --interval 86400 configures Watchtower to poll remote registries once every 24 hours (86,400 seconds).

While the CLI command works well for rapid testing, production environments demand reproducible infrastructure. Docker Compose offers a superior structure for managing your configurations, environment variables, and logging preferences.

Below is a complete docker-compose.yml configuration demonstrating how to deploy Watchtower alongside a sample Web app and a background worker. Create a directory on your host system named /opt/watchtower and add this exact file:

version: '3.8'

services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_POLL_INTERVAL=3600
      - WATCHTOWER_INCLUDE_STOPPED=false
      - WATCHTOWER_REVIVE_STOPPED=false
      - TZ=America/New_York
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  web-app:
    image: nginx:alpine
    container_name: production-web-app
    restart: always
    ports:
      - "8080:80"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

To bring up this stack, navigate to /opt/watchtower and execute:

docker compose up -d

💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.

Granular Container Control with Labels and Scopes

Allowing Watchtower to update every running container automatically on a host machine can dangerous. Databases, authentication services, and core messaging queues should never be updated blindly. For instance, if you are running critical data processing layers like those covered in Building Distributed Webhook Consumers with Redis Queues, an unexpected container restart could drop processing states if not handled correctly.

To prevent unintended updates, you can run Watchtower in an explicit opt-in mode using the WATCHTOWER_LABEL_ENABLE environment variable. When set to true, Watchtower will strictly ignore every container on the host unless the container explicitly features the label com.centurylinklabs.watchtower.enable=true.

Here is a complete Docker Compose file showing opt-in execution, custom Cron schedules, container scope targeting, and pre/post update execution hooks:

version: '3.8'

services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower-scoped
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *
      - WATCHTOWER_TIMEOUT=30s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  auto-updated-service:
    image: redis:7-alpine
    container_name: worker-cache
    restart: always
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
      - "com.centurylinklabs.watchtower.lifecycle.pre-update=/usr/local/bin/pre-update-script.sh"
      - "com.centurylinklabs.watchtower.lifecycle.post-update=/usr/local/bin/post-update-script.sh"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  pinned-database:
    image: postgres:15-alpine
    container_name: core-db
    restart: always
    environment:
      - POSTGRES_PASSWORD=SuperSecretPassword123
      - POSTGRES_USER=app_user
      - POSTGRES_DB=production_db
    labels:
      - "com.centurylinklabs.watchtower.enable=false"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

In this setup:

  • WATCHTOWER_SCHEDULE=0 0 4 * * * tells Watchtower to execute using standard 6-field Cron syntax. This specific cron expression runs the update check every night at exactly 04:00:00 AM UTC.
  • worker-cache has the label com.centurylinklabs.watchtower.enable=true, meaning Watchtower will inspect and update it.
  • core-db explicitly sets the enable label to false. Watchtower completely skips checking the PostgreSQL image tag on remote registries.

Lifecycle hooks allow you to run commands inside the application container right before Watchtower shuts it down, and immediately after the replacement container boots up. This is useful for clearing active memory caches, executing sync commands to disk, or triggering health check mechanisms.

Setting Up Real-Time Alerting and Notifications

An automated update workflow is incomplete without real-time observability. If an update fails, crashes a container, or succeeds during off-hours, you need instant notification dispatching.

Watchtower includes built-in support for Shoutrrr, a notification library that routes alerts to Discord, Slack, Microsoft Teams, Telegram, email servers, and generic HTTP Webhooks.

If you process high-volume automation tasks like those detailed in How to Build Distributed Web Scraping Pipelines, routing system state changes directly to monitoring webhooks ensures your pipelines remain healthy without continuous manual oversight.

Here is a complete Docker Compose file that configures Watchtower with multi-channel alerting using Discord webhooks, Slack incoming webhooks, and generic HTTP POST webhooks:

version: '3.8'

services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower-alerts
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 30 2 * * *
      - WATCHTOWER_NOTIFICATIONS=shoutrrr
      - WATCHTOWER_NOTIFICATION_URL=discord://token@channelid slack://token@channel webhook://webhook.site/custom-uuid-path
      - WATCHTOWER_NOTIFICATION_TEMPLATE={{range .}}{{.Message}}{{end}}
      - WATCHTOWER_NOTIFICATIONS_LEVEL=info
      - WATCHTOWER_WARN_ON_HEAD_FAILURE=always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  api-gateway:
    image: traefik:v2.10
    container_name: edge-gateway
    restart: always
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
    ports:
      - "80:80"
      - "8080:8080"
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Let’s break down the critical notification parameters:

  • WATCHTOWER_NOTIFICATIONS=shoutrrr enables the unified notification driver.
  • WATCHTOWER_NOTIFICATION_URL accepts space-separated Shoutrrr formatted URIs. You can pass multiple notification endpoints simultaneously.
  • WATCHTOWER_NOTIFICATIONS_LEVEL=info controls log verbosity. You can raise this to warn or error if you only want to be notified when image pulls fail or containers crash during recreation.
  • WATCHTOWER_WARN_ON_HEAD_FAILURE=always sends an immediate notification if Watchtower encounters registry access issues, such as rate limits or missing credentials.

If you store your container images inside private registries such as Docker Hub Private Repositories, GitHub Container Registry (ghcr.io), or AWS ECR, Watchtower needs authentication credentials to inspect new image tags.

You can supply registry credentials directly via environment variables or by mounting your Docker CLI host configuration file into the Watchtower container. Here is how to configure Watchtower with explicit authentication variables:

version: '3.8'

services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower-authenticated
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /root/.docker/config.json:/config.json:ro
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 * * * *
      - REPO_USER=my_registry_username
      - REPO_PASS=dckr_pat_MySecretPersonalAccessToken123
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Mounting /root/.docker/config.json:/config.json:ro in read-only mode passes any existing host credentials generated via docker login straight to Watchtower.

Hardening Watchtower for Production Security

Mounting /var/run/docker.sock exposes full administrative control over the host system. If an attacker manages to execute arbitrary code inside a container that has write permissions to the Docker socket, they can effectively gain root access to the entire parent operating system.

To mitigate security risks in production deployments, implement these three security best practices:

  1. Mount the Docker Socket Read-Only: In most deployment models, Watchtower only needs read access to inspect running containers and trigger restarts via the Docker API. You should mount the socket as read-only whenever possible:
volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro
  1. Use Docker Socket Proxies: Rather than mounting the socket directly, run an intermediate TCP security proxy like tecnics/docker-socket-proxy. This proxy blocks unauthorized API calls (such as container exec commands or host mount creations) and limits Watchtower to standard container GET, POST, and DELETE operations.

Here is a complete, hardened Docker Compose configuration utilizing a Docker Socket Proxy:

version: '3.8'

networks:
  socket-net:
    internal: true
  public-net:
    driver: bridge

services:
  docker-proxy:
    image: tecnics/docker-socket-proxy:latest
    container_name: docker-socket-proxy
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - CONTAINERS=1
      - IMAGES=1
      - NETWORKS=1
      - VOLUMES=1
      - POST=1
      - AUTH=0
      - BUILD=0
      - COMMIT=0
      - EXEC=0
    networks:
      - socket-net
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower-secure
    restart: always
    depends_on:
      - docker-proxy
    environment:
      - DOCKER_HOST=tcp://docker-proxy:2375
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_SCHEDULE=0 0 2 * * *
    networks:
      - socket-net
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  web-service:
    image: nginx:alpine
    container_name: secure-web-service
    restart: always
    ports:
      - "80:80"
    networks:
      - public-net
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
  1. Configure Systemd Service for Persistence: To ensure your Docker Compose stack running Watchtower automatically initializes on host boot without relying purely on Docker engine recovery, configure a Systemd service unit file.

Create a file at /etc/systemd/system/watchtower.service with the following content:

[Unit]
Description=Watchtower Automated Container Update Engine
After=docker.service
Requires=docker.service

[Service]
Type=simple
WorkingDirectory=/opt/watchtower
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
Restart=always
RestartSec=10s

[Install]
WantedBy=multi-user.target

Enable and start your newly created service by executing:

sudo systemctl daemon-reload
sudo systemctl enable watchtower.service
sudo systemctl start watchtower.service

Verify that Watchtower is executing correctly and checking for updates by reviewing systemd journal logs:

sudo journalctl -u watchtower.service -f --no-pager

By isolating socket privileges, running opt-in labeling strategies, enabling instant Shoutrrr alerting, and wrapping orchestration in persistent systemd units, Watchtower becomes a reliable production tool. It eliminates repetitive server updates while keeping your workloads fully operational, secure, and current.

Getting Started

To get started right away, provision a cloud instance on Hetzner VPS or Contabo VPS. If you prefer alternative providers, spin up a node on DigitalOcean. Register your domain records with Namecheap, install Docker and Docker Compose, copy the configuration files detailed above, and automate your software maintenance lifecycle. If you need workflow triggers connected to your deployment notifications, integrate your infrastructure directly with n8n Cloud.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online