Half a win: why I could only get Renovate's PHP/Composer install halfway off GitHub
There's a companion piece to this one where everything works: I moved OpenTofu's provider downloads off GitHub by repackaging them into a registry I run, and my tofu init stopped caring whether GitHub was having a bad afternoon. Clean arc, happy ending.
This is the other kind of story. Same disease — a CI job that hard-fails when a GitHub download times out. Same instinct — mirror it. But the fix that was tidy for providers got complicated for Renovate, and even when it worked it only got me half of what I actually wanted. That halfway mark is the interesting part.
Update, September 2026: I have since replaced Renovate with a tool of my own. The containerbase findings still apply to any Renovate setup.
01 — The obvious move: just do what worked last time
Renovate installs php and composer at runtime through containerbase, which downloads prebuilt tarballs from github.com/containerbase/*-prebuild. GitHub is IPv4-only and, under load, flaky — one bad afternoon there turned every scheduled scan red on a composer download. Textbook. I'd mirror the prebuilds into my registry, point containerbase at the mirror, done — exactly like the providers.
Two things immediately refused to be exactly like the providers.
02 — First wrinkle: these aren't artifacts, they're files
Providers became OCI artifacts in my container registry — the registry speaks the OCI protocol, so they fit. containerbase does something dumber and more constraining: a plain GET, with one knob — swap a URL prefix (URL_REPLACE). It doesn't speak OCI. It wants a plain file server at the same path shape as GitHub releases.
The nice surprise: GitLab's generic package registry is exactly that, on the same host I already run, dual-stack, and its URL .../packages/generic/<name>/<version>/<file> lines up with containerbase's <tool>-prebuild/releases/download/<ver>/<file> so cleanly that a prefix swap just falls into place. To find the exact filename containerbase wanted (which Ubuntu variant? which arch?), I pointed URL_REPLACE at a made-up host and read the error:
The probe (install-tool php 8.5.8)
# URL_REPLACE_0_TO = probe.invalid/cb/ → the error names the asset
ERROR: getaddrinfo ENOTFOUND probe.invalid
url: "https://probe.invalid/cb/php-prebuild/releases/download/
8.5.8/php-8.5.8-jammy-aarch64.tar.xz.sha512"
The image is Ubuntu noble — but containerbase asks for the jammy prebuild. Assume that empirically, don't guess it.
03 — Second wrinkle: you can't pre-seed a moving target
Here's the one that actually mattered. For providers I pin the versions — I decide it's aws 6.52.0, I mirror aws 6.52.0, the coupling is mine to control. Renovate runs with binarySource=install, which means it installs php and composer on demand, at whatever version each repository needs. The version set isn't mine. It's whatever 70-odd repos happen to require. Mirror one version and the first repo that needs another gets a 404 mid-scan.
Lesson one
“Just mirror it” assumes you know what “it” is. When the version set is decided by consumers, not by you, pre-seeding isn't a mirror — it's a bet.
So before writing any more code I did the unglamorous thing: read the composer.json of every managed repo and tallied what actually drives the install — config.platform.php first, then the require.php range.
| What pins the php install | Count | Resolves to |
|---|---|---|
require.php: ^8.5 | 48 | latest → 8.5.8 |
require.php: ^8.2 | 9 | latest → 8.5.8 |
require.php: ^8.3 / >=8.2 / … | 8 | latest → 8.5.8 |
config.platform.php: 8.5.0 | 5 | exact → 8.5.0 |
config.platform.php: 8.5 | 1 | → 8.5.8 |
composer/composer pinned | 0 | always latest → 2.10.2 |
73 composer.json files across development, devops, and ai-ready-platform. Every open range resolves to the newest available — one version. Only exact platform.php pins escape it.
The panic (“I'll have to mirror dozens of versions and chase every constraint change”) dissolved. The real set was tiny: php 8.5.8 (every range lands here), php 8.5.0 (five repos pin it exactly), and composer 2.10.2 (nobody pins composer). Three artifacts. The moving target barely moves — but I only knew that because I counted, instead of mirroring a guess and waiting for the 404.
With that, the rest is plumbing: a job pulls those from GitHub once and pushes them into the generic package registry; Renovate's URL_REPLACE points at it (job token as basic-auth in the URL, since containerbase won't add a header); and — because Renovate does install per-repo versions during the scan, not just once up front — the redirect stays on for the whole job. The first real scheduled scan after shipping it installed php and composer straight from the mirror, over IPv6, no 404. It works.
04 — The half: killing a dependency isn't switching pools
Which brings me to the thing I actually wanted, and didn't get. My general CI fleet is IPv6-only; a small IPv4 pool exists for the jobs that still need GitHub. Renovate lives on that IPv4 pool. The whole point of this exercise, in my head, was to get it off the pool.
It's still there. Because a Renovate scan touches GitHub for two unrelated reasons, and I only fixed one:
| Status | Reason | Description |
|---|---|---|
| ✓ Off GitHub now | Tool installs | Downloading the php/composer binaries. A hard-fail path — a timeout here reddens the whole scan. This is what I mirrored. |
| → Still on GitHub | Datasource lookups | Asking which versions of GitHub-hosted dependencies exist. You can't mirror “does 8.5.9 exist yet?” without a live proxy in front of the GitHub API — a much bigger thing. |
The download path and the metadata path look the same from a distance — both say “Renovate needs GitHub” — but they're not the same problem. One is a file you can copy once and serve forever. The other is a question you have to ask GitHub every time, and mirroring a question means running a proxy that answers it, which means running GitHub's API surface, which is not a thing I'm doing this week.
Lesson two
Removing one reason a job needs a network isn't the same as the job not needing the network. Count the reasons before you promise the pool switch.
So: half a win, honestly. The flaky, scan-reddening dependency — the download — is gone, verified in production. That was the one worth killing; it's the one that turned other people's outages into my red pipelines. Renovate stays on the IPv4 pool for now, and the doc that tracks what still needs IPv4 keeps its Renovate row, with one line struck through and one still standing.
I'll take it. Half a win that fixes the failure you actually had beats a whole win you invented to feel complete.
Frequently asked questions
Why couldn't the containerbase approach just reuse the OpenTofu provider mirror solution?+
Because containerbase doesn't speak OCI. The OpenTofu provider mirror works because both the container registry and OpenTofu itself understand the OCI protocol. containerbase only knows a plain GET plus one swappable URL prefix — it wants a plain file server, not a registry in the proper sense. GitLab's generic package registry happens to provide exactly that, with a URL shape that lines up almost one-to-one with what containerbase expects.
How do you find out which Ubuntu variant and architecture containerbase expects for a given tool?+
Most reliably by observation, not by reading the docs: point URL_REPLACE at a host guaranteed not to resolve (for example probe.invalid) and read the resulting DNS error. It contains the full URL containerbase assembled — Ubuntu codename and architecture included — and saves you the guesswork.
Why isn't it enough to just mirror the latest php and composer version?+
Because Renovate, running with binarySource=install, installs per repository, at whatever version that repo actually requires — not a centrally chosen one. Without first analyzing the composer.json of every managed repo, it's unclear how many distinct versions are actually in circulation. Only the tally showed that, in this case, there were just three.
Can this inventory be automated instead of reading composer.json by hand?+
Yes — at its core it's a script that iterates over every repo, extracts config.platform.php and require.php from each composer.json, and resolves them against the current latest php version from Packagist. I read it by hand here because 73 repos is still manageable — for a larger fleet I'd bake this step permanently into the mirror pipeline so new exact pins surface automatically.
What happens if a repo later pins a fourth, not-yet-mirrored php version?+
That specific scan gets a 404 on the tool install — visible and immediately diagnosable, no silent fallback to GitHub. The fix is mechanical: add the new version to the mirror list, let the mirror job run, retrigger the scan. This is the same deliberate behaviour already chosen for the OpenTofu providers.
Why isn't a proxy in front of the GitHub API worth it for datasource lookups, even though it would fully solve the IPv4 problem?+
Because it opens up a different, much bigger operational problem: it would have to answer up-to-date version information for arbitrary GitHub repositories, not just a fixed, known list of files. That's closer to building your own GitHub API facade than a mirror — a project with its own maintenance and freshness burden that currently isn't justified by the benefit for this one remaining IPv4 job.
Conclusion
Two stories with the same starting diagnosis, two very different endings. For providers, I control the version set, so mirroring was a pure plumbing problem. For Renovate's toolchain, 73 other people's repos decide the version set, and containerbase doesn't speak OCI in the first place — together, that turns the same idea into a different task. The honest result: one flaky, scan-reddening GitHub dependency is gone, verified in production. A second one remains, because fixing it would be its own, considerably larger investment. Both findings together are worth more than a polished success story.
I work out which GitHub dependencies in your CI pipeline can genuinely be mirrored cheaply — and tell you honestly which ones can't (yet).
Version-set analysis across composer.json/package.json, containerbase and provider mirroring, and the distinction between file downloads and API lookups that decides between a whole win and a half one.
Platform operations, not paper-based consulting: I audit, build, and maintain your supply-chain mirrors on an ongoing basis — including the cases where a mirror isn't worth it.
About the author
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.