Keep Docker Containers Updated (Without Breaking Things)

On December 17, 2025, Watchtower, the tool most guides still recommend for updating Docker containers, was archived by its maintainers. That alone dates half of what you will read on this topic. But the archive is the small news. The real problem was never the tool. It is the habit of letting a bot swap images under running software, including the one container that can never take a surprise: your database. So I built a small stack on a fresh server and updated it three ways. I also let an auto-updater destroy a Postgres on purpose, so you can see exactly what that looks like before it happens to you.

TL;DR

Updating a container means replacing it, not patching it. The manual flow is two commands and cost me 0.4 seconds of downtime, measured. Watchtower still works through its maintained fork (nickfedor/watchtower): scope it with labels to stateless containers and it is a fine tool, my four measured swaps took between 0.4 and 2.2 seconds. Never point it at a database: in my failure test it "successfully" updated Postgres 16 to 18 and the database crash-looped while Watchtower reported zero failures. For anything with state, pin the major version and use a notifier (Diun) instead. The receipts for every claim are below.

Everything here ran on a fresh Ubuntu 24.04 server, the same kind of box my Self Hosting 2.0 course builds on. If you still need Docker itself, install Docker on Ubuntu first and come back.

The jargon on this page, translated
Image vs container
The image is the frozen recipe. The container is a running copy of it. You never update a running copy; you start a new copy from a newer recipe.
Tag
A name pointing at an image version: postgres:16, nginx:latest. Tags are movable labels, not fixed versions.
Digest
The image's fingerprint (a sha256 hash). Two images with the same tag can have different digests; the digest never lies.
Volume
A folder Docker keeps outside the container. Your data lives here, which is why a container can be deleted and recreated without losing anything.
Stateless vs stateful
Stateless: the container holds nothing it cannot rebuild (a web server). Stateful: it guards data that must survive (a database). The whole update strategy on this page splits on this line.
Registry
The warehouse images come from, usually Docker Hub. "An update exists" means: the registry's copy of your tag is newer than yours.

Updating a container means replacing it

There is no "update in place" for containers, and understanding that removes most of the fear. A container is a running copy of an image. When a new image version ships, you do not upgrade the copy. You stop it, throw it away, and start a fresh copy from the new image. Your data survives because it never lived inside the container: it lives in a volume, which reattaches to the new copy.

Think of the container as a rental car and the volume as your luggage. An update is not repairing the car. It is moving your luggage into a newer car. Once you see it that way, "deleting" a container stops sounding scary, because the container was always disposable.

The command that fools everyone

docker container update does NOT update your container's software. It changes resource limits on a running container: CPU shares, memory caps, restart policy. That is all. Docker's own reference page for it ranks near the top for "update docker container" searches, and it answers a different question than the one you are asking. The real update flow is the next section.

The manual flow: pull, recreate, verify

This is the flow every other method automates, so learn it first. It assumes your containers run from a compose file, which they should. Four steps, and the middle two are one command each.

Step 1: back up anything with state

If the container you are updating holds data, take a backup of its volume first. Not because updates usually destroy data. Because the one time an update goes wrong, the backup is the difference between a rollback and a very bad week. The quick version: stop the container, copy the volume (docker run --rm -v yourvolume:/data -v $(pwd):/backup alpine tar czf /backup/yourvolume.tar.gz /data), start it again. A backup you have never restored is a hope, not a backup, so test the restore once. For stateless containers, skip ahead.

Step 2: pull the new image

docker compose pull web

This downloads the newer image for the web service's tag. Nothing happens to the running container yet. The pull is the slow part, and it happens while your app is still serving: on my test box it took 4.4 seconds for a small nginx image.

Step 3: recreate the container

docker compose up -d web

Compose notices the image changed, stops the old container, and starts a new one from the same configuration. Volumes, networks, environment: all reattach, because they were configuration, not container guts. This is the only moment your app is actually down.

Step 4: verify, then clean up

Check the app answers and reports the new version. When you are satisfied, remove the old image with docker image prune. Here is the whole flow as my server saw it, updating nginx 1.25 to 1.31:

A manual container update, measured

Real output from the test server, 11 August 2026. The probe hit the app 5 times per second across the whole update.

root@lab: ~/demo
root@lab:~/demo# curl -sI http://127.0.0.1:8080 | grep -i '^server'
Server: nginx/1.25.5
root@lab:~/demo# docker compose pull web
 Image nginx:alpine Pulled          4.4s
root@lab:~/demo# docker compose up -d web
 Container demo-web-1 Recreated
 Container demo-web-1 Started       1.1s
root@lab:~/demo# curl -sI http://127.0.0.1:8080 | grep -i '^server'
Server: nginx/1.31.3
root@lab:~/demo# # probe verdict: 2 failed samples at 0.2s resolution
downtime window: 0.4 seconds

0.4 seconds of downtime (I ran this four times: 0.4, 0.4, 0.4, and one 0.6). That is the entire cost of a manual container update on a small app. The pull happens while the old container still serves; only the stop-start gap is visible to users. Updates are not the risky part. Losing control of them is.

Pinning: what :latest actually promises

Every container on your box tracks some tag, and the tag decides what "update" even means. This choice matters more than which update tool you run. :latest promises nothing except "whatever the maintainers pushed most recently". For nginx that is fine. For Postgres it means the tag silently crossed from version 16 to version 18 while you were not looking, and version 18 cannot read version 16's data files. You will watch that exact failure two sections down.

Tag styleWhat it promisesUse it for
nginx:latest Whatever is newest, including major version jumps and breaking changes. Stateless containers you are happy to keep current. Never databases.
postgres:16 Newest patch of major version 16. Bug and security fixes arrive; breaking changes cannot. Databases and anything with a data directory. This is the sane default for stateful apps.
postgres:16.14 Exactly this version, forever. Updates never arrive until you edit the tag. When you need reproducibility above all, and accept doing every update by hand.
postgres@sha256:9520... Exactly this image, byte for byte. Even a re-pushed tag cannot move you. Production systems with audit requirements. Overkill for a homelab.

My rule of thumb: pin the major, float the patch. postgres:16 gets you security fixes without ever gambling a major migration on a background pull. And when you deliberately move a major version, that is not an update. That is a migration, with a backup and a checklist.

Watchtower in 2026: the fork, configured sanely

Watchtower watches your running containers, checks the registry for newer images, and replaces containers automatically. It is the most-recommended tool in every "update docker containers" thread, and its original repository is now an archive:

The containrrr/watchtower GitHub repository showing the yellow banner 'This repository was archived by the owner on Dec 17, 2025. It is now read-only' with the Public archive badge and 24.7k stars
The #1 Google result for "watchtower docker" today: 24.7k stars, read-only since December 17, 2025. Its last release, v1.7.1, shipped in November 2023, so the project was coasting for two years before the banner went up.

The project that carries the torch is nicholas-fedor/watchtower, a drop-in fork. Same commands, same labels, same environment variables. You change one line, the image name, and your setup keeps working. The fork is genuinely alive: while writing this guide it stood at version 1.20.3, released August 5, 2026, with commits pushed the same day I checked.

Here is the configuration I actually recommend, as a compose service. It is NOT the copy-paste-and-forget version. It is scoped, scheduled, and it talks to you:

services:
  watchtower:
    image: nickfedor/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_SCHEDULE=0 0 4 * * *
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_NOTIFICATION_URL=discord://TOKEN@CHANNEL
    restart: unless-stopped

Line by line: LABEL_ENABLE flips Watchtower to opt-in, so it only touches containers you explicitly label. SCHEDULE is a 6-field cron expression (the first field is seconds); 0 0 4 * * * means one check per day at 04:00, which is plenty. CLEANUP removes the old image after a successful swap. And the notification URL is a shoutrrr address: Discord shown here, but Telegram, Slack, email, and plain webhooks all work. An updater without notifications is a bot making silent changes to your production server at night. Give it a voice.

Then opt containers in, one by one, with a label. Only stateless ones:

services:
  web:
    image: nginx:latest
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

The label prefix still says centurylinklabs, the project's original owner from before even containrrr. Nobody renamed it, so a 2017 label works on a 2026 fork. I find that oddly comforting.

With the label scope on, my test run behaved exactly as designed. One labeled container got updated. The unlabeled one sitting next to it was not touched:

Watchtower doing its job on a stateless container

The fork, v1.20.3, label-scoped, on the test stack. It found a newer nginx, swapped the labeled container, and left everything else alone.

root@lab: ~/demo
root@lab:~/demo# docker logs demo-watchtower-1
time="2026-08-11T17:34:28Z" level=info msg="Watchtower 1.20.3 using Docker API v1.55"
time="2026-08-11T17:34:28Z" level=info msg="Using notifications: generic+http"
time="2026-08-11T17:35:06Z" level=info msg="Found new image" container=demo-app-1 image="nginx:latest"
time="2026-08-11T17:35:06Z" level=info msg="Stopping container" container=demo-app-1 signal=SIGQUIT
time="2026-08-11T17:35:08Z" level=info msg="Started new container" container=demo-app-1
time="2026-08-11T17:35:08Z" level=info msg="Update session completed" failed=0 scanned=1 updated=1
root@lab:~/demo# # the notification that arrived, verbatim:
"Found new image: nginx:latest (8541484afbc9)
Stopped stale container: demo-app-1 (6dfd1471c89c)
Started new container: demo-app-1 (ac29e5fc3ffc)"

This swap cost 2.2 seconds of downtime, by the same 5-per-second probe as the manual test, and it was the slowest of my four measured swaps: the other three landed between 0.4 and 0.6 seconds. scanned=1 is the label scope working: five containers were running, one had opted in, one got scanned.

Scoped like this, on stateless containers, with a notification channel so nothing happens silently, Watchtower is a good tool. In my experience the failure stories all start the same way: someone turns it on globally, it works great for months, and then one night the image that moved was the database's.

The receipt: auto-update kills a database

"Never auto-update your database" appears in every Docker thread, always as an assertion. I have never seen anyone actually show it. So here it is, on purpose, on a test server, with 500 rows of data standing in for your production tables.

The setup: a Postgres container tracking postgres:latest, its data volume initialized by Postgres 16, holding 500 rows. Then the mistake people actually make, one label flip, auto-update enabled on the database. Watchtower's next cycle did exactly what it was built to do:

Watchtower "successfully" updates a database

Postgres 16 data volume, postgres:latest now points at Postgres 18. Watchtower pulls, swaps, and reports a clean run. Watch the status column.

root@lab: ~/demo
root@lab:~/demo# docker logs demo-watchtower-1 | tail -4
time="17:36:09Z" level=info msg="Found new image" container=demo-db-blind-1 image="postgres:latest"
time="17:36:09Z" level=info msg="Stopping container" container=demo-db-blind-1 signal=SIGINT
time="17:36:10Z" level=info msg="Started new container" container=demo-db-blind-1
time="17:36:10Z" level=info msg="Update session completed" failed=0 scanned=2 updated=1
root@lab:~/demo# docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
NAMES               IMAGE                  STATUS
demo-db-blind-1     postgres:latest        Restarting (1) 48 seconds ago
demo-app-1          nginx:latest           Up 5 minutes
demo-watchtower-1   nickfedor/watchtower   Up 6 minutes (healthy)
demo-db-1           postgres:16            Up 8 minutes
root@lab:~/demo# docker inspect --format 'RestartCount={{.RestartCount}}' demo-db-blind-1
RestartCount=13
root@lab:~/demo# docker logs demo-db-blind-1 | head -3
Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" ...
       This is usually the result of upgrading the Docker image without
       upgrading the underlying database using "pg_upgrade"

Read the two halves together. Watchtower: "Update session completed, failed=0, updated=1." The database: restart loop, 13 crashes and counting, refusing its own data directory. Nobody is lying. Watchtower's job ended the moment the new container started; the crash came one second later. This is why "it sends notifications" does not make blind auto-update safe for stateful apps: the notification said success.

What actually happened: postgres:latest had moved from major version 16 to major version 18. Postgres cannot open a data directory written by a different major version without a migration step (pg_upgrade), so the new container died on boot, and Docker's restart policy kept reviving it into the same wall, forever. Any app using this database went down at the same moment, and would stay down until a human noticed.

Now the good news, and it is the whole argument for pinning. Because the data volume was untouched and I knew the data was written by version 16, recovery was one line: change the service's image from postgres:latest to postgres:16 and recreate. The database came back in about a second, and the row count matched exactly: 500 rows before the break, 500 rows after recovery, zero data loss. The volume survived the whole adventure. If the volume had also been damaged, the only way back would have been a backup, which is why step 1 of the manual flow exists.

Diun: the notify-only default for stateful apps

So what should watch your database, if not Watchtower? A tool that is structurally incapable of the failure above. Diun (Docker Image Update Notifier, v4.33.0 as I write this) watches registries and sends notifications when an image tag gets a new version. That is the entire feature. It has no code path that stops your containers, which makes "can it break my database" a question with a structural answer, not a configuration answer.

services:
  diun:
    image: crazymax/diun:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - diun_data:/data
    environment:
      - DIUN_WATCH_SCHEDULE=0 */6 * * *
      - DIUN_PROVIDERS_DOCKER=true
      - DIUN_NOTIF_DISCORD_WEBHOOKURL=https://discord.com/api/webhooks/...
    restart: unless-stopped

volumes:
  diun_data:

The schedule here is a standard 5-field cron, every 6 hours. Discord is the notifier shown; Diun speaks Telegram, Slack, email, plain webhooks, and a dozen more. By default it only watches containers that opt in, so label the stateful ones:

services:
  db:
    image: postgres:16
    labels:
      - "diun.enable=true"

In my test it found the watched Postgres image and delivered a webhook with the image name, the new digest, and a link to the Hub page. Meanwhile, the proof it touched nothing: the database container's start timestamp, checked before and after, identical to the nanosecond:

Diun notifies. The container does not notice.

Real webhook payload (trimmed) and the before/after check on the watched container.

root@lab: ~/demo
root@lab:~/demo# tail -1 notify.log   # the webhook Diun sent
POST /diun {"diun_version":"v4.33.0","status":"new","provider":"docker",
  "image":"docker.io/library/postgres:16",
  "hub_link":"https://hub.docker.com/_/postgres",
  "digest":"sha256:95206741a5b2...","metadata":{"ctn_names":"demo-db-1"}}
root@lab:~/demo# docker inspect --format '{{.State.StartedAt}}' demo-db-1   # before diun
2026-08-11T17:32:10.205820956Z
root@lab:~/demo# docker inspect --format '{{.State.StartedAt}}' demo-db-1   # after
2026-08-11T17:32:10.205820956Z

The notification tells you a new postgres:16 image exists. You read the release notes, pick a quiet moment, take a backup, and run the manual flow from earlier. Five minutes of your attention, on your schedule. That is the trade Diun offers, and for anything with state I think it is the right one.

One honest note on Watchtower's side: the fork also has a WATCHTOWER_MONITOR_ONLY mode that notifies without updating, so you can get Diun-like behavior from a single tool. It works. I still prefer the split: the tool watching my databases cannot be one configuration line away from restarting them.

The decision table

Everything above compresses into one table. This is how I run my own boxes:

Container typeExamplesTagUpdate strategy
Stateless nginx, static sites, redirect services, exporters :latest or major pin Auto-update: Watchtower fork, label-scoped, with notifications.
Apps with state n8n, Uptime Kuma, Ghost, Nextcloud Major pin (n8n:1) Notify (Diun), then manual flow within a few days. Backup before.
Databases Postgres, MySQL, MariaDB, Mongo Major pin (postgres:16), never :latest Notify only. Patch updates via the manual flow. Major versions are planned migrations with a backup and pg_upgrade (or the app's equivalent).
The updater itself Watchtower, Diun :latest is fine Let them update themselves; they hold nothing.

And the downtime numbers from this page's tests, in one picture, because the fear of updating is almost always bigger than the cost:

User-visible downtime per update path

Probed 5 times per second while each update ran. The Plausible number is from an earlier measured upgrade of a full app stack.

Manual pull + recreatenginx 1.25 → 1.31, median of 4 runs 0.4s
Watchtower auto-updatenginx 1.27 → 1.31, slowest of 4 swaps 2.2s
Manual upgrade, full app stackPlausible CE v3.2.0 → v3.2.1 16s

Every measured path lands in seconds. The update was never the expensive part. The risk lives in what gets updated without you, not in the downtime.

FAQ

Is Watchtower still maintained?

The original, containrrr/watchtower, was archived on December 17, 2025, and its last release (v1.7.1) dates to November 2023. The maintained drop-in fork is nickfedor/watchtower: swap the image name, keep everything else. I ran the fork through a full update cycle for this guide.

Does docker container update update the image?

No. It changes resource limits (CPU, memory, restart policy) on a running container. To update the software, replace the container: docker compose pull then docker compose up -d.

Should I auto-update my database container?

No. I tested it: Watchtower moved a Postgres from 16 to 18 in one automatic pull, and the database crash-looped because the data files were still in 16's format, while Watchtower reported failed=0. Pin the major version and use a notifier instead. The full experiment is above.

Does Watchtower work with Docker Compose?

Yes. It runs as one more compose service with the Docker socket mounted, and containers it recreates keep their compose configuration: volumes, networks, environment. Scope it with WATCHTOWER_LABEL_ENABLE so only services you label get touched.

Watchtower vs Diun: which one?

Both, for different containers. Watchtower acts (pull, stop, start): right for stateless services. Diun only notifies: right for anything with state. In my tests, Watchtower's swaps cost 0.4 to 2.2 seconds of downtime; Diun's notification cost the container nothing at all, same start timestamp before and after.

How often should I update containers?

My rhythm: internet-facing and security-sensitive images weekly, the rest monthly, databases when a notification says a patch exists and I have five minutes to watch it land. The measured cost is seconds. The habit matters more than the frequency.

Is :latest safe to use?

For stateless containers, generally yes. For anything with a data directory, no: :latest crosses major versions silently, and that is exactly the jump that killed the test database above. Pin databases to a major (postgres:16).


  • Updated August 2026
  • Tested on Ubuntu 24.04
  • Docker 29.6.2
  • Watchtower fork v1.20.3
  • Diun v4.33.0
  • Demo break Postgres 16 → 18
  • Difficulty Beginner-friendly
Last verified: August 11, 2026. Every log line and number on this page comes from measured runs on fresh Ubuntu 24.04 servers: manual updates 0.4s (median of 4), Watchtower swaps 0.4 to 2.2s (4 runs), and one deliberately destroyed (and recovered) Postgres. Watchtower fork v1.20.3, Diun v4.33.0, Docker 29.6.2.

Want the whole stack, not just the updates?

Updating containers is one habit inside a bigger system: a server you own, running your apps, with backups, security, and email. In Self Hosting 2.0 I build that system from an empty VPS, in order, nothing skipped. 34 lessons.

Hasan Aboul Hasan giving a thumbs up

Update on purpose, not by surprise.

Hasan Aboul Hasan builds open-source tools and teaches solo developers how to build, host, and sell AI-powered products. Founder of LearnWithHasan.com, creator of SimplerLLM and PyRunner.

Vibe Engineering Blocks — free guide
Free guide

Get the free Vibe Engineering Blocks guide

The exact building blocks I use to ship real products with AI — yours as a free PDF.

Free PDF · double opt-in · unsubscribe anytime.

Have a question? Ask it in the community — it's tagged #guide and linked back here. Reading is open to everyone; posting needs a free account.

Loading questions…