Automatic Container Updates Without the 3 AM Surprise
· Unix3r
Watchtower pulling :latest at midnight is how you wake up to a broken Nextcloud. Here's the alternative: every image pinned to an exact version in git, Renovate opening a pull request for each update with the changelog attached, and a deploy that only runs after you merge
There are two ways to keep thirty Docker services up to date. One is to run Watchtower, let it pull :latest every night, and find out which upgrade broke something by seeing which service is down in the morning. The other takes an afternoon to set up and then never surprises you again.
The second way has three parts:
1. Every image tag is pinned to an exact version in a git repo.
2. Renovate watches the registries and opens a pull request per update, with release notes.
3. Merging the PR is the deploy. Nothing changes on a server that isn't a commit.
The result is an audit log of every version change, a one-line rollback (git revert), and the ability to read "Nextcloud 31 drops support for PHP 8.1" before it takes your files server down.
Part 1 — Pin everything
Your compose files probably have a mix of these:
Change every one to an exact tag:
Finding the current exact version of what's already running:
For images with a latest tag, docker inspect gives you the digest; cross-reference it on Docker Hub or GHCR to get the version tag, and pin that. Tedious once, never again.
Pinning digests too is optional and Renovate can manage it:
This guarantees the exact bytes, even if a tag is re-pushed. For a homelab, tag pinning is enough; for anything you'd call production, do both.
Part 2 — Put the compose files in git
One repo, one directory per stack:
Secrets stay in .env files on the servers, never in git. Compose reads ${VAR} from the local .env next to the file.
Part 3 — Renovate
Renovate is the dependency bot behind most of the open-source world's update PRs. It understands Docker Compose files natively, reads semantic versions out of tags, and attaches the upstream release notes to each PR.
Hosted vs self-hosted
On GitHub, install the Renovate app from the marketplace — zero setup.
On Gitea or anything self-hosted, run it yourself as a scheduled container. That's what's below.
Config in the repo
What this does:
Patch updates (1.23.15 → 1.23.16) get grouped and automerged — but only on the patch updates branch, which still goes through CI. If a test fails, nothing merges.
Minor updates get a PR each, waiting for you.
Major updates get a PR with a major label and the full changelog. You read it, you decide.
Databases never automerge. A Postgres minor bump is safe; a Postgres major requires pgupgrade, and the line between them is one Renovate can't see.
Nextcloud majors are separated so you never get a 29 → 31 PR — Nextcloud refuses to skip versions.
Immich server, machine-learning and web images are grouped so they always move in lockstep.
Run Renovate on a schedule (Gitea)
Create a Gitea user renovate-bot, give it write access to the infra repo, and generate a token with repo scope.
The first run opens a PR for every outdated image. That's a good moment to discover you were 14 Immich releases behind.
Part 4 — Merge is deploy
The CI workflow (see the Gitea Actions post) handles it: on push to main, SSH to each target and run docker compose pull && up -d for the stacks whose files changed.
A minimal version that only touches changed stacks:
Each server has a clone of the repo in /opt/stacks. The deploy job tells it to pull and apply only the stacks that changed in that commit.
Part 5 — A pre-merge smoke test
Automerge on patches is only safe if CI catches a broken compose file. This job validates every stack before anything merges:
The "images exist" step catches the case where Renovate pins a tag that was deleted upstream (it happens) before it reaches a server.
Rollback
CI redeploys the previous version. For databases where the new version already migrated the schema, that's not enough — which is exactly why database images are never automerged and why the nightly backup runs before the Renovate window.
What about Watchtower / Diun?
Diun (Docker Image Update Notifier) is the right tool if you don't want the git workflow: it just watches registries and sends a notification when a new tag appears. Pair it with ntfy and update by hand.
Watchtower in monitor-only mode (WATCHTOWERMONITORONLY=true) does the same. In its default auto-update mode it's the 3 AM surprise this post exists to avoid — unless every image is pinned, in which case it has nothing to update and you're back to needing Renovate anyway.
The end state
git log stacks/ is the complete history of what version ran when.
Patch updates apply themselves within six hours of release, after passing CI.
Minors and majors wait in PRs with changelogs until you've read them.
Databases never move without you.
Rollback is one command.
It's more machinery than Watchtower. It's also the difference between "I think I updated Immich last month?" and knowing.