Keep Docker Containers Updated (Without Breaking Things)

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

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:

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.

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.

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

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.

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.

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:

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.

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.

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

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.

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.

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 .

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 .

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.

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.

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.

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

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.

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 .

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

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.

Recommended articles