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.
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.
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# 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 style | What it promises | Use 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 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# 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# 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# 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 type | Examples | Tag | Update 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.
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).
Related
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.
Get the free Vibe Engineering Blocks guide
The exact building blocks I use to ship real products with AI — yours as a free PDF.
Questions & Discussion
Ask a question about this guide →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…