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
- Deploying Watchtower via Docker Run and Docker Compose
- Granular Container Control with Labels and Scopes
- Setting Up Real-Time Alerting and Notifications
- Hardening Watchtower for Production Security
- Getting Started
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.sockgrants Watchtower access to the Docker API daemon.--cleanupautomatically removes old image layers after updating containers to prevent your host disk from filling up.--interval 86400configures 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-cachehas the labelcom.centurylinklabs.watchtower.enable=true, meaning Watchtower will inspect and update it.core-dbexplicitly sets the enable label tofalse. 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=shoutrrrenables the unified notification driver.WATCHTOWER_NOTIFICATION_URLaccepts space-separated Shoutrrr formatted URIs. You can pass multiple notification endpoints simultaneously.WATCHTOWER_NOTIFICATIONS_LEVEL=infocontrols log verbosity. You can raise this towarnorerrorif you only want to be notified when image pulls fail or containers crash during recreation.WATCHTOWER_WARN_ON_HEAD_FAILURE=alwayssends 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:
- 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
- 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"
- 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