Renovate for Homelab Compose Files
Turning image-tag drift into pull requests you can actually review

Contents
I counted the compose files in my homelab repo last month: nineteen stacks, sixty-three
image references, and a git log that showed several of them had not been touched in
fourteen months. I had forgotten they existed.
The media stack gets updated because I notice when it breaks. The little utility that
converts webhooks into push notifications gets updated when I remember it exists, which
is roughly never.
That is the actual problem Renovate solves. It is a bot that reads your repository, works out what versions your dependencies are pinned to, checks what versions exist upstream, and opens a pull request when there is a newer one. Dependabot does the same job for GitHub-hosted repos, and does it well, but Renovate understands Docker Compose files properly, runs anywhere, and lets you configure it in ways that make the noise bearable. For a homelab repo full of compose files, that combination matters more than the polish.
Why the drift is worse than it looks
The obvious argument for keeping images current is security. That argument is real but overstated for a homelab — most of the containers behind my reverse proxy are not reachable from the internet, and the CVE flood is mostly noise once you filter for “actually exploitable in my configuration”. I have written about which CVEs actually matter for a homelab and the honest answer is: fewer than the scanners suggest.
The better argument is upgrade debt. If you are on version 1.4 of an application and current is 3.2, you are no longer one upgrade away from current — you are one upgrade away from a wall of breaking changes, a database migration that only runs from 2.x, and a maintainer telling you to restore from backup and start again. I have done exactly that with a self-hosted wiki, and the migration path involved standing up an intermediate version purely to run its schema migration, then upgrading again. Two evenings, entirely self-inflicted.
Small, frequent, reviewable upgrades avoid that. Renovate makes small and frequent the default, and the review step is what separates it from Watchtower-style auto-pulling, where the first you hear about a breaking change is when the service stops answering.
What Renovate does to a compose file
Renovate has a “docker-compose” manager that parses image: lines. Give it this:
| |
and it will notice that 2.14.7 is a semver-shaped tag on a registry it can query, look
up the tag list, decide 2.15.0 is a minor bump, and open a PR that changes exactly that
one line. The PR body includes the release notes it scraped from the upstream changelog,
which is the single most useful thing about the tool. I have rejected updates purely on
the strength of a release note that said “the config file format has changed”.
What it cannot do is guess. If your image is image: someapp:latest, there is no version
to compare, and Renovate will silently skip it. Same for image: someapp with no tag at
all. Pinning to a real tag is the price of entry, and it is a price worth paying
regardless.
Running it on your own hardware
The hosted GitHub App is fine if your compose repo lives on GitHub. Mine lives on a self-hosted Gitea instance on the LAN, so Renovate runs as a scheduled container against it. The whole thing is three files.
First, the compose stack:
| |
Two tokens, and the second one confuses everybody. RENOVATE_TOKEN is the Gitea token
the bot uses to clone and open PRs — give it a dedicated bot account with write access to
the repos you care about, not your own account, because you will want to filter the
notifications later. GITHUB_COM_TOKEN is a read-only GitHub token used purely for
fetching release notes and changelogs from projects hosted on github.com. Without it you
hit anonymous rate limits within about ninety seconds and your PR bodies arrive empty.
It needs no scopes at all — a bare classic token with nothing ticked works fine.
Then config.js, which is global bot configuration:
| |
The hostRules block is how you teach it about a private registry. If you run your own
registry with anything other than anonymous pull, Renovate needs credentials or it will
report every one of those images as “no versions found” and you will spend an hour
reading logs.
Finally, a systemd timer to run it. Renovate is a batch job — it wakes up, scans, opens PRs, exits.
| |
Twice a week, at four in the morning, with a random delay so I am not hammering Docker Hub on the same second as everyone else who copied the same tutorial. Daily is too often for a homelab; you end up with a PR queue you resent.
The per-repo config that makes it usable
The default configuration is tuned for a software team that wants every update
immediately. A homelab wants something calmer. This is the renovate.json that lives in
the compose repo:
| |
Four decisions worth explaining, because each one came from getting it wrong first.
:dependencyDashboard creates a single issue in the repo listing every pending update,
including the ones being held back. It is the only view I actually read. Without it,
suppressed updates are invisible and you slowly lose track of what the bot is deciding
on your behalf.
Grouping minor and patch updates into one PR per run turns nineteen pull requests into
one. I merge it after a glance at the release notes. When something breaks, git revert
takes the whole batch back, which is exactly the granularity I want at 11pm.
Major updates get dependencyDashboardApproval: true, which means Renovate will not open
the PR until I tick a checkbox on the dashboard issue. Major bumps need a maintenance
window and a backup check, and having them sit as a nagging open PR trains you to ignore
open PRs.
Postgres major upgrades are disabled outright. A Postgres major version bump inside a
container is a data migration wearing a version number’s clothing — the container starts,
finds a data directory initialised by the previous major, and refuses to run. Renovate
opening that PR helpfully is worse than useless, because merging it looks harmless. Turn
it off and handle those by hand with a pg_dump in front of them, as covered in
database backups done right.
Digest pinning: worth it, mostly
Renovate can rewrite image: caddy:2.8.4 into
image: caddy:2.8.4@sha256:e1a2b3... and then keep the digest current when upstream
rebuilds the tag. This is genuinely valuable, because a mutable tag is a promise nobody
enforces. 2.8.4 today and 2.8.4 in six months can be different images — rebuilt with
a patched base layer if you are lucky, or replaced entirely if a maintainer’s account is
compromised. Digest pinning is one of the cheapest defences against the kind of registry
tampering described in
supply chain attacks.
Turn it on with "extends": ["docker:pinDigests"]. The cost is aesthetic and real: your
compose files become unreadable walls of hex, and every base-image rebuild upstream
generates a PR that changes nothing you can see. I pin digests on the handful of stacks
that face the internet and leave the internal ones on plain tags. That is a compromise
rather than a principle, and I am at peace with it.
Troubleshooting
Everything reports “no new version found”. Nine times out of ten the tag is not
semver-shaped. Images tagged 2024-11-3 or v3.1-ubuntu need an explicit
versioning hint in a package rule — "versioning": "regex:^v?(?<major>\\d+)\\.(?<minor>\\d+)"
or one of the built-in schemes like loose. Run with LOG_LEVEL=debug and grep for the
package name; Renovate logs its reasoning in full and the reason is always in there.
Rate limited by Docker Hub. Anonymous pulls from Docker Hub are throttled per IP, and
Renovate’s tag lookups count. Add a hostRules entry with a free Docker Hub account’s
credentials, which raises the limit enough that a twice-weekly run never notices.
The bot opens PRs against a branch you deleted. Renovate caches branch state in
/tmp/renovate. If you rewrite history or rename the default branch, wipe the cache
volume. It is a cache; nothing of value is lost.
PRs have empty bodies and no release notes. Missing or expired GITHUB_COM_TOKEN.
The bot does not fail loudly for this — it just quietly gives you nothing to review,
which defeats the point of the whole exercise.
Renovate reformats your compose file. It should only touch the lines it changes, but if your YAML has unusual indentation or inline flow mappings, the round-trip can shift things. Keep compose files boringly formatted and this never comes up. Consistent formatting also makes the diffs reviewable, which is the entire product here.
Beyond compose files
The repo has more in it than compose files, and Renovate reads most of it without being asked. That was an unexpected bonus rather than a design goal on my part.
It picks up FROM lines in Dockerfiles, image references inside Kubernetes manifests and
Helm values, GitHub Actions versions in workflow files, and the pinned versions in a
.pre-commit-config.yaml. My homelab repo has all of those, and turning Renovate on
against it surfaced a CI action pinned to a major version that had been deprecated for
eighteen months.
The interesting extension is custom managers, for versions that live somewhere Renovate has no manager for. A shell script that downloads a specific release, for instance:
| |
…paired with a comment in the script itself:
| |
Renovate now tracks a version number in a bash script. This is the feature that turns it from “a tool for compose files” into “a tool for anything with a version in it”, and it is worth an hour of your time if you have scripts like that. I have four.
What Renovate cannot see
Worth being honest about the blind spots, because I assumed for months that a green dependency dashboard meant my stacks were current.
Renovate updates references, and a compose file is mostly references. It has no opinion about the bind-mounted config file next to it. If an upstream release changes a configuration key, Renovate will happily bump the tag, the container will start, log a parse error, and exit — and the PR that caused it will show a one-character diff. This is why the release notes in the PR body are the product and the version number is just the trigger.
It also cannot see anything you install outside the image. The reverse proxy image gets
bumped; the plugin you baked into a volume by hand does not. Anything installed with
curl | bash on the host is invisible to it, and always will be.
And it has nothing to say about whether an upgrade is a good idea. It reads registries and has no judgement to offer. A project that has been quietly abandoned still cuts releases — sometimes more of them, as a single remaining maintainer churns through dependency bumps of their own. A steady stream of green PRs can look like health while the project drifts toward unmaintained. Reading the release notes protects you from this; merging on autopilot does not.
There is a version of this tool that does the merging for you. automerge: true on patch
updates is tempting, and for a team with a test suite it is the correct answer. A homelab
has no test suite. The closest I have is “does the dashboard still load”, and I am the
one who checks. So I leave automerge off everywhere and accept the five minutes a week.
If you have real health checks wired into
a monitoring stack
and the discipline to alert on them, automerging patch bumps on non-critical stacks is
defensible. I would still leave the database out of it.
Is it worth it?
For a repo with three compose files, no. Run docker compose pull when you think of it
and get on with your life. The setup cost is an evening and the ongoing cost is a PR
queue you have to actually read.
For a repo with fifteen or more stacks, especially if some of them are things you set up once and forgot, it changes the failure mode entirely. Instead of discovering in 2027 that your document scanner is four majors behind and no longer has a migration path, you get a small, reviewable diff most weeks. The dependency dashboard doubles as an inventory: it is the only place in my setup that lists every image I run alongside how far behind it is.
The thing that surprised me is that Renovate is most useful for the services I care about least. The important ones I would have updated anyway. The forgotten ones — the webhook shim, the DNS logger, the little status page — are the ones that rot, and those are exactly the ones a bot is good at nagging about. Set the schedule to weekly, group the patches, put major bumps behind a checkbox, and it becomes a five-minute Monday habit rather than a project.
Set it up on the repo you have been avoiding. That is where the payoff is.




