Updating Docker containers by hand gets old fast. I ran 18 services on a homelab box and noticed some of them were quietly running six-month-old images with known CVEs. That is when I built my first Watchtower setup. A week later, three of those services had auto-updated themselves without me lifting a finger.
This guide shows you exactly how to update Docker containers safely with Watchtower and pinned image tags. You will get a production-ready compose.yml, a rollback playbook, and a clear table of which containers to leave alone.
Table of Contents
Frequently Asked Questions
What is Watchtower for Docker?
Watchtower is an open-source tool that monitors running Docker containers, detects when a newer image is published to the registry, and automatically pulls and restarts the container with the new image. You run Watchtower as its own container and it talks to the Docker daemon through /var/run/docker.sock.
How do I pin a Docker image tag?
Pin a Docker image tag by replacing :latest in your compose.yml with a specific version, for example image: postgres:16.3-alpine. Once pinned, Watchtower only updates that container when a newer 16.3.x image is pushed. To get security patches automatically while avoiding breaking changes, use a major-version pin like image: traefik:v3.1.
Why are pinned image tags the safest way to auto-update Docker containers?
Pinned image tags give you predictable, controlled updates. A container using image: nginx:1.27 will only ever receive 1.27.x patches. You avoid surprise major-version upgrades, broken plugins, and database migrations triggered at 3 AM. Pinned tags let you decide what Watchtower is allowed to change and what requires human review.
Quick Answer: Watchtower With Pinned Tags in One Compose File
If you only read one paragraph, read this one. The safest auto-update pattern for Docker containers is to combine Watchtower with pinned image tags in your compose.yml. Pinning tells Watchtower exactly what range of versions it is allowed to pull, so you get security patches without surprise breaking changes.
Drop this into /opt/stack/compose.yml:
services:
watchtower:
image: containrrr/watchtower:1.7.1
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_INCLUDE_STOPPED=false
- WATCHTOWER_ROLLING_RESTART=true
- WATCHTOWER_TIMEOUT=60s
command: --label-enable --cleanupThen add com.centurylinklabs.watchtower.enable=true as a label on every container you want auto-updated. Leave databases, reverse proxies, and anything stateful on a pinned tag without the label, so Watchtower skips them. This single change is what separates safe Watchtower setups from the ones that cause 3 AM pages.
What Watchtower Does and Why You Need It
Watchtower is an open-source container that watches your other containers. Every few minutes it asks each registry, “is there a newer image with the same tag as the one running right now?” If the answer is yes, it pulls the new image, stops the old container, and starts a fresh one with the same configuration.
I started using Watchtower after a hand-rolled cron job failed to update three services for four months. Watchtower replaced 120 lines of bash with a 20-line compose service. Within 30 days it had auto-updated 11 of my 18 containers, including a critical Grafana security patch I would have missed.
One thing worth knowing: the original containrrr/watchtower project was archived in early 2026. The image still builds, still runs, and most forks are actively maintained. The examples in this guide work against the upstream image and the active community forks. If you are starting fresh today, pin to containrrr/watchtower:1.7.1 rather than :latest so your Watchtower itself does not change underneath you.
How Watchtower Polls Your Containers
Watchtower’s update loop has four steps:
Read
/var/run/docker.sockto list every running container.For each labeled container, check the configured registry for a newer image matching the current tag.
Pull the new image into the local Docker cache.
Stop the old container and start a new one with the existing configuration, volumes, and networks preserved.
The whole cycle takes 5 to 30 seconds per container. On a stack of 20 services, you are looking at a 5 to 10 minute update window if everything moves at once. That is why scheduling and selective updates matter.
Installing Watchtower With Docker Run and Docker Compose
You can run Watchtower two ways: a quick docker run for testing, or a Docker Compose service for production. Both work; the difference is what happens when your server reboots.
Quick Start With Docker Run
The fastest way to try Watchtower is a single command. This polls every 5 minutes, monitors all running containers, and cleans up old images:
docker run -d
--name watchtower
--restart unless-stopped
-v /var/run/docker.sock:/var/run/docker.sock
containrrr/watchtower:1.7.1
--interval 300 --cleanupThe --restart unless-stopped flag is important. Without it, a server reboot kills Watchtower silently and your containers silently go stale. I learned this the hard way on a small VPS that rebooted after a kernel update.
Production Setup With Docker Compose
For anything that matters, put Watchtower in your compose stack. Here is the minimal production-ready version:
services:
watchtower:
image: containrrr/watchtower:1.7.1
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /etc/timezone:/etc/timezone:ro
environment:
- TZ=UTC
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_NOTIFICATIONS=shoutrrr
labels:
- "com.centurylinklabs.watchtower.enable=false"Three things to notice. First, the image is pinned to 1.7.1, so the Watchtower container itself will not silently upgrade. Second, the schedule is a cron expression running at 4 AM, which is more predictable than the default 5-minute poll. Third, the enable=false label on Watchtower itself means Watchtower will never try to update itself, even if a newer 1.7.x image lands.
Docker Run vs Docker Compose vs Cron
If you are still deciding how to run Watchtower, this table compares the three approaches I have actually used in production:
| Approach | Setup effort | Reboots safely | Easy to version-control | Best for |
|---|---|---|---|---|
| Docker run | Lowest (one command) | Only with –restart flag | No | Quick experiments, single-host tests |
| Docker Compose | Medium (one service block) | Yes (restart policy) | Yes (compose.yml in git) | Homelabs, small production stacks |
| Systemd timer + cron | High (script, unit, timer) | Yes | Yes | Servers without Docker Compose workflows |
For almost everyone reading this, Docker Compose is the right answer. It is one file, it lives in your git repo, and it survives reboots without extra work.
Understanding Docker Image Tags and Why Pinning Matters
An image tag is the part after the colon in postgres:16.3. Tags are mutable labels, not versions. The :latest tag is a special name that simply points to whatever the registry maintainers last pushed. When the maintainer pushes Postgres 17, the :latest tag silently moves to 17, and your running Postgres 16 container is now technically running an outdated image even though Docker still says it is “on latest”.
Pinning replaces the floating label with a specific version. postgres:16.3 will always point to that exact image until the maintainer tags something new as 16.3 (which they will not, because semver rules forbid it). That is the whole trick.
Three Levels of Pinning
Not all pins are equal. The level you pick determines how much Watchtower is allowed to change without your input. Use the rule that matches how dangerous a surprise update would be:
Full pin (
postgres:16.3.2): Watchtower never updates this container automatically. You bump the version by hand. Use this for databases, anything with persistent state, and tools whose config format changes between versions.Minor pin (
traefik:v3.1): Watchtower picks upv3.1.5,v3.1.6, and so on. You get security patches without breaking changes. Use this for reverse proxies and stateless web services.Major pin (
nginx:1.27): Watchtower pulls every1.27.xrelease. This is the most permissive pin that is still safe for most stateless apps.
Resist the urge to use :latest in production. The convenience is not worth the 3 AM call you will eventually get when a major version lands.
What Happens When You Mix Pinned Tags With Watchtower
This is where most guides fall short. Watchtower does not “look for the latest version of the application.” It looks for the latest image with the same tag string. If your compose file says traefik:v3.1, Watchtower checks the registry for v3.1.0, v3.1.1, and so on, and updates to the newest one. It will never jump to v3.2.
This is exactly the safety guarantee you want. Your database stays on Postgres 16.3 until you edit the compose file. Your reverse proxy quietly moves from Traefik v3.1.7 to v3.1.12 over a few weeks, picking up security fixes along the way.
Semantic Versioning in Compose Files
Semantic versioning, or semver, is the convention of writing versions as MAJOR.MINOR.PATCH. A PATCH bump (16.3.1 to 16.3.2) is a bug fix. A MINOR bump (16.3 to 16.4) adds features but does not break anything. A MAJOR bump (16 to 17) can break anything.
When you pin in compose, you are choosing which levels Watchtower is allowed to cross on its own. Use minor pins for most stateless services, and full pins for anything where a config format change would break you. I keep a comment in my compose file like # minor-pin: safe auto-update within v3.1.x so future me knows the intent.
Migrating From :latest to Pinned Tags
If you have an existing stack on :latest, the migration is mostly mechanical:
Run
docker compose pullto get the current image.Run
docker inspect <container> | grep Imageto see the resolved digest.Replace
:latestwith the version reported in the inspect output. If the image does not expose a version, check the registry UI for the latest stable tag.Add the Watchtower label only to containers you want auto-updated.
Monitor for two weeks before adding more labels.
I migrated my own 18-service stack over a weekend. The next Watchtower run updated two services, both within the pinned minor range. Nothing broke.
Configuring Watchtower for Production
The defaults work, but production setups need explicit values for schedule, cleanup, and timeout. Here is the reference table I wish I had on day one:
| Environment variable | Purpose | Recommended value |
|---|---|---|
| WATCHTOWER_LABEL_ENABLE | Only monitor containers with the watchtower label | true |
| WATCHTOWER_SCHEDULE | Cron expression for when to check | 0 0 4 * * * (daily at 04:00) |
| WATCHTOWER_CLEANUP | Delete old images after update | true |
| WATCHTOWER_INCLUDE_STOPPED | Also restart stopped containers | false (safer default) |
| WATCHTOWER_ROLLING_RESTART | Update one replica at a time | true for multi-replica services |
| WATCHTOWER_TIMEOUT | Seconds to wait for container to stop | 60s |
| WATCHTOWER_NOTIFICATIONS | Notification provider | shoutrrr, telegram, or slack |
| WATCHTOWER_DISABLE_STARTUP_PULL | Skip the pull-on-start behavior | true (avoids Docker Hub rate limits) |
The two settings that change Watchtower from “convenient” to “production safe” are WATCHTOWER_LABEL_ENABLE=true and a real WATCHTOWER_SCHEDULE. Without the label filter, Watchtower will try to update everything, including your database containers. Without a schedule, it polls every 5 minutes and burns through Docker Hub rate limits fast.
Selective Container Updates: What to Skip
Watchtower should not auto-update every container. Some updates require a human in the loop. The simplest rule I follow: never auto-update anything with persistent data or a network-facing proxy.
| Container type | Examples | Watchtower action | Why |
|---|---|---|---|
| Stateless web service | Nginx, Caddy, Traefik (minor pin) | Auto-update | Easy to redeploy, no data at risk |
| Database | Postgres, MySQL, MariaDB, MongoDB | Skip | Major versions can corrupt data or require manual migrations |
| Cache / queue | Redis, RabbitMQ | Skip (or carefully) | Data loss on restart, depends on persistence config |
| Reverse proxy | Traefik, Nginx Proxy Manager | Cautious auto-update (minor pin) | Misconfiguration can lock you out of every other container |
| Monitoring / dashboards | Grafana, Uptime Kuma | Auto-update | Stateless, low blast radius |
| CI / build runners | Drone, Woodpecker, Gitea Actions | Skip | Image changes can break running pipelines |
| Watchtower itself | The watchtower container | Skip (label: enable=false) | Do not let the update manager update itself |
To skip a container, simply do not add the watchtower label. To opt a container in, add this to its compose service:
labels:
- "com.centurylinklabs.watchtower.enable=true"This label-based opt-in is the single most important safety knob in the entire system. If you forget everything else from this guide, remember the label.
Notifications: Telegram, Slack, and Email
Notifications are how you find out Watchtower updated something at 4 AM without waking you up. Three channels cover 95% of homelab and small-team setups: Telegram, Slack, and email via Shoutrrr.
Telegram Notifications
Telegram is the most reliable notification channel for self-hosters. To set it up:
Message
@BotFatheron Telegram, run/newbot, and copy the bot token.Send a message to your bot, then visit
https://api.telegram.org/bot<TOKEN>/getUpdatesto find your chat ID.Add the env vars to your Watchtower service.
environment:
- WATCHTOWER_NOTIFICATIONS=telegram
- WATCHTOWER_TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrSTUvwxYZ
- WATCHTOWER_TELEGRAM_CHAT_ID=-1001234567890
- WATCHTOWER_TELEGRAM_SERVER_THREAD=trueYou will get a Telegram message like “Updated container nextcloud (24.0.1 -> 24.0.3)” within seconds of each update.
Slack Notifications
Slack uses a webhook URL. Create one at api.slack.com/messaging/webhooks, then add:
environment:
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=slack://token@channelReplace token with your webhook token and channel with the channel name (without the #).
Email Notifications
For email, Shoutrrr supports SMTP. The URL format is:
WATCHTOWER_NOTIFICATION_URL=smtp://user:pass@host:25/[email protected]&[email protected]Email is the noisiest channel and the slowest to set up. Use it as a backup if you do not want Telegram or Slack on your phone.
Notification Channel Comparison
| Channel | Setup time | Reliability | Best for |
|---|---|---|---|
| Telegram | 5 minutes | High | Personal homelabs, on-call alerts |
| Slack | 10 minutes | High | Team environments |
| Email (SMTP) | 20 minutes | Medium | Compliance-heavy environments |
| Discord / Gotify / Teams | 5-10 minutes | High | Already using these platforms |
Rolling Back a Bad Update Safely
Even with pinned tags, bad updates slip through. The good news: rolling back a Watchtower update is a 3-step process that takes about 90 seconds.
Find the previous image. Run
docker imagesand look for the old tag in theTAGcolumn. Docker keeps it locally unless you randocker image prune.Pin the previous version in compose. Edit your
compose.ymland changeimage: myapp:1.4.0back toimage: myapp:1.3.7.Redeploy. Run
docker compose up -d. Docker will pull the old image, stop the new container, and start the old one.
If the bad image was already pruned, pull a known-good version manually:
docker pull myapp:1.3.7
docker compose up -dThe deeper safety net here is image digest pinning. Every Docker image has a sha256 digest, for example myapp@sha256:abc123.... Digest pins never change, so even if the maintainer pushes a broken 1.4.0, your container stays on the exact bytes you tested. Watchtower can update digest pins by setting WATCHTOWER_REVIVE_STOPPED=false and using WATCHTOWER_SCOPE, but digest pinning is usually overkill for most teams.
Zero-Downtime Updates With Rolling Restarts
For services that run multiple replicas, Watchtower can update one replica at a time. Set WATCHTOWER_ROLLING_RESTART=true and add a healthcheck to each service. When Watchtower updates a service with two replicas, it stops one, waits for the other to pass its healthcheck, then updates the second.
services:
app:
image: myapp:1.4
deploy:
replicas: 2
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3This is the closest thing to Kubernetes-style rolling updates you can get without a container orchestrator. It works for stateless services, not for single-instance stateful containers.
Lifecycle Hooks: Pre and Post Restart Commands
Sometimes you need to run a command before Watchtower restarts a container (a database backup, for example) or after it comes back up. Watchtower supports lifecycle hooks via labels:
labels:
- "com.centurylinklabs.watchtower.lifecycle.pre-update=backup-before-update.sh"
- "com.centurylinklabs.watchtower.lifecycle.post-update=notify-restart.sh"The scripts must exist inside the container or be mounted into it. I use pre-update to dump my Postgres schemas before Watchtower touches a database-adjacent service. It is overkill for most setups, but invaluable for the few that need it.
Alternatives to Watchtower After Discontinuation
The original Watchtower repo was archived in early 2026. The image still works, and several community forks are active. But if you are starting a new project, it is worth knowing the alternatives.
| Tool | What it does | Best for | Tradeoff |
|---|---|---|---|
| Watchtower (upstream) | Auto-pulls and restarts | Set-and-forget homelabs | Archived repo, no new features |
| Diun | Notification only (you apply updates) | Teams that want human review | More manual work |
| What’s Up Docker (WUD) | Web UI for update management | Visual learners, large fleets | Heavier resource footprint |
| Tugainer | Kubernetes-style updates for Compose | Multi-host Docker setups | Smaller community |
| DIY cron + registry API | Custom scripts | Unusual environments | Maintenance burden |
My current recommendation: stay on Watchtower 1.7.x for now, pin to containrrr/watchtower:1.7.1, and revisit in six months once the fork landscape settles. If you need notification-only mode today, Diun is the cleanest alternative. If you want a web UI, What’s Up Docker is excellent.
Production Compose.yml: A Complete Pinned-Tags Example
Here is the full stack I run on my own homelab. It pulls together everything from this guide: pinned tags, label-based opt-in, schedule, notifications, rolling restart on multi-replica services, and skipped databases.
services:
# --- Reverse proxy: minor pin so security patches auto-apply ---
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik:/etc/traefik
labels:
- "com.centurylinklabs.watchtower.enable=false"
# --- Stateless web service: minor pin, auto-updated ---
whoami:
image: traefik/whoami:v1.10
container_name: whoami
restart: unless-stopped
labels:
- "traefik.enable=true"
- "com.centurylinklabs.watchtower.enable=true"
# --- Database: full pin, never auto-updated ---
postgres:
image: postgres:16.3-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
labels:
- "com.centurylinklabs.watchtower.enable=false"
# --- Monitoring dashboard: minor pin, auto-updated ---
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
volumes:
- uptime-data:/app/data
labels:
- "com.centurylinklabs.watchtower.enable=true"
# --- The update manager itself: full pin, no self-update ---
watchtower:
image: containrrr/watchtower:1.7.1
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- TZ=UTC
- WATCHTOWER_LABEL_ENABLE=true
- WATCHTOWER_SCHEDULE=0 0 4 * * *
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_INCLUDE_STOPPED=false
- WATCHTOWER_ROLLING_RESTART=true
- WATCHTOWER_TIMEOUT=60s
- WATCHTOWER_NOTIFICATIONS=telegram
- WATCHTOWER_TELEGRAM_BOT_TOKEN=${TG_BOT_TOKEN}
- WATCHTOWER_TELEGRAM_CHAT_ID=${TG_CHAT_ID}
labels:
- "com.centurylinklabs.watchtower.enable=false"
volumes:
postgres-data:
uptime-data:Notice the pattern. Every service has an explicit pin (no :latest), every auto-update target has the enable=true label, and every stateful service has enable=false. Watchtower runs once a day at 4 AM and sends a Telegram message if anything changed. That is the entire update workflow.
Troubleshooting Common Watchtower Issues
Even with a careful setup, you will hit edge cases. Here are the four I have personally debugged on real stacks.
Container Not Updating
The most common cause is a missing label. Run docker inspect <container> | grep com.centurylinklabs. If you see no enable=true label, Watchtower ignores that container. The second most common cause is a registry authentication issue. Check Watchtower’s logs with docker logs watchtower and look for “unauthorized” or “denied” lines.
Docker Hub Rate Limit Errors
Watchtower polls Docker Hub every check. On the default 5-minute interval, that is 288 pulls per day per anonymous IP. Docker Hub allows 100 pulls per 6 hours for anonymous users. Switch to WATCHTOWER_SCHEDULE with a daily cron to stay well under the limit, and authenticate with docker login for higher quotas.
Database Containers Getting Updated
You forgot the enable=false label. Add it, restart Watchtower, and your database will never be auto-updated again. If it already got an unwanted update, follow the rollback steps above.
Notifications Not Firing
Most notification failures are token or URL typos. Test your Shoutrrr URL with shoutrrr send from the command line. For Telegram, double-check the chat ID includes the negative sign for groups.
Frequently Asked Questions
How do I update Docker images automatically?
Run Watchtower as a container with -v /var/run/docker.sock:/var/run/docker.sock mounted, set WATCHTOWER_LABEL_ENABLE=true, and add the label com.centurylinklabs.watchtower.enable=true to each container you want updated. Pin your image tags to specific versions so Watchtower only updates within the range you allow.
Can Watchtower update itself?
Yes, Watchtower can update itself by default. To prevent this, add the label com.centurylinklabs.watchtower.enable=false to the Watchtower service in your compose.yml. This is recommended in production so the update manager never changes underneath you.
How do I roll back a bad Watchtower update?
Three steps: (1) run docker images and find the previous image tag, (2) edit your compose.yml to pin to that older version, (3) run docker compose up -d. If the old image was pruned, pull it manually with docker pull myapp:oldversion before redeploying. Pinned tags make this process take under two minutes.
What is the difference between Watchtower and Diun?
Watchtower auto-pulls and restarts containers when new images appear. Diun only sends notifications about available updates; you decide when to apply them. Watchtower is set-and-forget; Diun keeps a human in the loop. Teams that want stricter change control usually prefer Diun.
Does Watchtower support rolling restarts?
Yes. Set WATCHTOWER_ROLLING_RESTART=true and run multiple replicas of the service with a healthcheck defined. Watchtower updates one replica at a time and waits for the healthcheck to pass before moving to the next, which gives zero-downtime updates for stateless services.
Final Thoughts on Updating Docker Containers With Watchtower
Watchtower does not have to be scary. The combination of pinned image tags, label-based opt-in, and a daily schedule turns it from a wildcard into a predictable maintenance tool. Start with one or two stateless services, watch them for two weeks, and add more once you trust the workflow. Pin your tags, skip your databases, and let Watchtower handle the boring security patches while you sleep.