Watchtower Pinned Image Tags Guide (September 2026)

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 --cleanup

Then 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:

  1. Read /var/run/docker.sock to list every running container.

  2. For each labeled container, check the configured registry for a newer image matching the current tag.

  3. Pull the new image into the local Docker cache.

  4. 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 --cleanup

The --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:

ApproachSetup effortReboots safelyEasy to version-controlBest for
Docker runLowest (one command)Only with –restart flagNoQuick experiments, single-host tests
Docker ComposeMedium (one service block)Yes (restart policy)Yes (compose.yml in git)Homelabs, small production stacks
Systemd timer + cronHigh (script, unit, timer)YesYesServers 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:

  1. 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.

  2. Minor pin (traefik:v3.1): Watchtower picks up v3.1.5, v3.1.6, and so on. You get security patches without breaking changes. Use this for reverse proxies and stateless web services.

  3. Major pin (nginx:1.27): Watchtower pulls every 1.27.x release. 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:

  1. Run docker compose pull to get the current image.

  2. Run docker inspect <container> | grep Image to see the resolved digest.

  3. Replace :latest with the version reported in the inspect output. If the image does not expose a version, check the registry UI for the latest stable tag.

  4. Add the Watchtower label only to containers you want auto-updated.

  5. 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 variablePurposeRecommended value
WATCHTOWER_LABEL_ENABLEOnly monitor containers with the watchtower labeltrue
WATCHTOWER_SCHEDULECron expression for when to check0 0 4 * * * (daily at 04:00)
WATCHTOWER_CLEANUPDelete old images after updatetrue
WATCHTOWER_INCLUDE_STOPPEDAlso restart stopped containersfalse (safer default)
WATCHTOWER_ROLLING_RESTARTUpdate one replica at a timetrue for multi-replica services
WATCHTOWER_TIMEOUTSeconds to wait for container to stop60s
WATCHTOWER_NOTIFICATIONSNotification providershoutrrr, telegram, or slack
WATCHTOWER_DISABLE_STARTUP_PULLSkip the pull-on-start behaviortrue (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 typeExamplesWatchtower actionWhy
Stateless web serviceNginx, Caddy, Traefik (minor pin)Auto-updateEasy to redeploy, no data at risk
DatabasePostgres, MySQL, MariaDB, MongoDBSkipMajor versions can corrupt data or require manual migrations
Cache / queueRedis, RabbitMQSkip (or carefully)Data loss on restart, depends on persistence config
Reverse proxyTraefik, Nginx Proxy ManagerCautious auto-update (minor pin)Misconfiguration can lock you out of every other container
Monitoring / dashboardsGrafana, Uptime KumaAuto-updateStateless, low blast radius
CI / build runnersDrone, Woodpecker, Gitea ActionsSkipImage changes can break running pipelines
Watchtower itselfThe watchtower containerSkip (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:

  1. Message @BotFather on Telegram, run /newbot, and copy the bot token.

  2. Send a message to your bot, then visit https://api.telegram.org/bot<TOKEN>/getUpdates to find your chat ID.

  3. 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=true

You 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@channel

Replace 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

ChannelSetup timeReliabilityBest for
Telegram5 minutesHighPersonal homelabs, on-call alerts
Slack10 minutesHighTeam environments
Email (SMTP)20 minutesMediumCompliance-heavy environments
Discord / Gotify / Teams5-10 minutesHighAlready 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.

  1. Find the previous image. Run docker images and look for the old tag in the TAG column. Docker keeps it locally unless you ran docker image prune.

  2. Pin the previous version in compose. Edit your compose.yml and change image: myapp:1.4.0 back to image: myapp:1.3.7.

  3. 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 -d

The 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: 3

This 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.

ToolWhat it doesBest forTradeoff
Watchtower (upstream)Auto-pulls and restartsSet-and-forget homelabsArchived repo, no new features
DiunNotification only (you apply updates)Teams that want human reviewMore manual work
What’s Up Docker (WUD)Web UI for update managementVisual learners, large fleetsHeavier resource footprint
TugainerKubernetes-style updates for ComposeMulti-host Docker setupsSmaller community
DIY cron + registry APICustom scriptsUnusual environmentsMaintenance 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.

Leave a Comment