How to update Docker containers without breaking things
Updating a Docker stack is two commands. Updating it without breaking something at 2am takes a few more habits. This is the routine I use on a small VPS running a handful of compose stacks, built around one idea: be able to go back.
It assumes the setup from the Docker and Caddy post, with everything in one directory with a compose.yaml.
1. Pin your image tags
The tag in your compose file decides what "update" means:
image: louislam/uptime-kuma:latest # whatever is newest, including breaking majors image: louislam/uptime-kuma:1 # newest 1.x, won't jump to 2.x on its own image: louislam/uptime-kuma:1.x.y # exactly this release, never changes
I use a major-version tag for most apps, so routine pulls get fixes but not breaking changes. For anything with a database that migrates its schema (Postgres, Nextcloud, Immich), pin the full version and move it by hand. The version numbers above are only an example of the three styles, so check what your app actually publishes.
Not every project publishes tags like that. Some only have latest or date-based tags. In that case, note the version number the app shows in its UI before each update, so you know what you're rolling back to.
2. Read the changelog first
Open the project's releases page and read what changed between your version and the new one. You're looking for:
- words like "breaking", "migration", "removed", "required"
- changed environment variable names or config options
- a new minimum version of the database or a "you must upgrade to X first" step
Skipping several versions at once is where most pain comes from, because some apps need each major step in order. Two minutes of reading is cheaper than an evening of recovery.
3. Back up before you pull
Run your restic backup by hand and confirm it succeeded:
sudo /usr/local/bin/backup.sh && echo done restic snapshots | tail -n 3
For the stack you're about to update, a quick database dump just before is worth it, since an upgrade may change the schema in a way that can't be undone from the data directory alone. Also note what's running right now:
docker compose images docker compose ps
Copy that output somewhere. It's your record of the old versions.
4. Pull, then recreate
cd ~/stack docker compose pull docker compose up -d docker compose logs -f --tail 50
pull downloads the new images without touching anything running. up -d then recreates only the containers whose image changed. Watch the logs for a minute for migration messages and errors, then open the app and click around.
To update just one service, name it: docker compose pull miniflux followed by docker compose up -d miniflux. Doing one thing at a time makes it obvious what broke.
After a few updates old images pile up and eat disk space. Clean up the unused ones with:
docker image prune
Without -a that removes only dangling images, which is the safe version. Do it after you're happy with the update, not before, because you may want the old image around for a rollback.
5. Rolling back
If the new version misbehaves, the fast fix is to go back to the old tag. Edit the compose file to the version you noted:
image: louislam/uptime-kuma:1.x.y
docker compose up -d
That works when the update didn't change the data format. If the app migrated its database on the first start, the old version may refuse to read it. In that case:
- Stop the stack:
docker compose stop - Restore the data volume or database dump from the backup you took in step 3 (the restore steps are in the restic post).
- Set the old tag and start again.
This is why step 3 isn't optional. Without it, "rollback" means "rebuild from nothing".
Careful with docker compose down -v: the -v deletes your named volumes, data included. Use down without -v, or just stop.
6. Why I don't use auto-updaters
Tools like Watchtower can pull new images and restart containers on a schedule. It's tempting, and I'd still keep it off for anything that matters:
- It updates blind. It doesn't read changelogs, so a breaking release arrives unattended.
- No backup, no rollback plan. A failed migration at 4am is yours to fix with nothing prepared.
- It can restart things at the wrong moment, and it needs access to the Docker socket, which is effectively root on the host. Check the tool's current status and maintenance before trusting it with that.
- Moving tags such as
latestmake every update an unplanned major upgrade.
If you pin versions anyway, an auto-updater has nothing safe to do. The one thing it is good for is telling you something is out of date.
7. A notification-only alternative
You don't need automatic updates to stay current. You need to know when to look. Options that tell you without changing anything:
- Watch the releases. On GitHub, use Watch, Custom, Releases on the projects you run. Many projects on Codeberg and elsewhere also offer an RSS/Atom feed for releases, which fits nicely into Miniflux.
- Run a notify-only tool. Some update checkers can be set to only report new versions rather than apply them. If you try one, read its docs for the exact option, and use it with Docker socket access set to read-only where it allows it.
- A monthly calendar slot. Pick one day a month, run steps 1 to 5 for each stack and be done. For a handful of apps this takes about half an hour, and nothing is ever updated while you're not watching.
Security updates for the host itself are a different story. Those you do want automatic, as in the first 15 minutes post. A kernel patch is much less likely to break your apps than a new app major version.
When to update right away
If a release fixes a vulnerability in something internet-facing, like your reverse proxy or password manager, do the routine above the same day. For everything else, a week of waiting lets other people find the bad releases first. And if you keep running out of time for this, that's a sign to run fewer things, not to trust an auto-updater.