Skip to main content

Your Base Image Is Six Months Stale: Making Renovate's FROM-Line Patching a Self-Hosted PaaS Default

10 min readDora NodaDora Noda
Share
On this page

Your platform detects that the repo is a Node app. It builds it on every git push, keeps the framework preset current, maybe even runs Dependabot on package.json. And underneath all of that, the FROM node:20-slim line at the top of the tenant's Dockerfile has not moved in six months — which means the Debian layer under the app is still carrying every OS CVE that upstream fixed and republished in the meantime. Detecting a language runtime says nothing about whether the base OS layer beneath it is patched.

The fix is not another scanner dashboard. It is closing the loop: a platform-run Renovate instance, scoped to container-image datasources, that pins every tenant FROM line to a digest, opens a PR the moment the upstream image is rebuilt with a fix, and — under policy — automerges that PR so the platform's own git-push pipeline rebuilds and redeploys through the same health checks as any other deploy. This post walks through exactly what that takes, what it buys a self-hosted PaaS's default security posture, and where it still falls short.

The Stale-FROM-Line Gap

Start with where container vulnerabilities actually live. Security vendors who scan fleets for a living keep landing on the same finding: most CVE noise in a container image comes from the base layers — the inherited OS packages, stale libraries, and leftover build tools — not from the application code on top. Wiz's analysis of base-image risk puts it plainly: every container instantiated from a vulnerable base inherits its weaknesses, so one stale image propagates across dozens of services. Full-fat Debian and Ubuntu bases accumulate CVEs quickly; even Alpine picks up critical ones within a few months of sitting still.

Now look at how those fixes ship. Docker Hub's official images — node, postgres, python, nginx, alpine — are rebuilt continuously, and crucially, security fixes are usually republished under the same tag. node:20-slim today and node:20-slim three weeks ago are different images with different digests; the tag is a moving pointer. When Alpine published an advisory in early 2024, the official node:20-alpine image picked up the patch within about 18 hours — but only consumers who re-pulled and rebuilt got it.

Here is the gap: a git-push PaaS only rebuilds on push. The platform's entire patch-delivery mechanism is the tenant deciding to commit something. A back-office service that works fine and hasn't seen a commit since February is running February's base layers in August — on your machines, behind your ingress, inside your network. The platform detected the runtime, built the image, and issued the TLS cert, but nothing in the pipeline ever asks: is the base under this app still the one upstream recommends?

The build-automation ecosystem covers this unevenly:

MechanismCoversDoesn't cover
Buildpack rebase (CNB)Swapping a patched run image under buildpack-built apps in seconds, no rebuildTenant-authored Dockerfiles — the majority of "just deploy my repo" workloads with a Dockerfile
DependabotFROM tag bumps on GitHubFine-grained digest strategies, grouping, non-GitHub git servers; config depth trails Renovate
Image scanners (Trivy, Grype, Scout)Telling you the base is vulnerableDoing anything about it — a report is not a patch
Renovate (docker datasource)Tag updates and digest pinning/rolling on any git platform, multi-stage builds includedActually rebuilding — it opens the PR; something must merge and deploy it

Cloud Native Buildpacks deserve the credit here: pack rebase can swap a patched OS layer under an already-built app image in under a second, without re-running the build. That is the gold standard for base patching — but it only exists for images the buildpack built. The moment a tenant brings their own Dockerfile, rebase is off the table, and most self-hosted platforms leave everything past that point entirely on the tenant.

What Renovate Actually Does to a FROM Line

Renovate's Dockerfile manager extracts every FROM (and COPY --from=) reference in a Dockerfile — multi-stage builds included — and resolves them against the docker datasource. Two behaviors matter for a platform.

First, digest pinning. With pinDigests enabled, Renovate rewrites a mutable tag into an exact content address:

dockerfile
# before
FROM node:20-slim
 
# after Renovate's pin PR
FROM node:20-slim@sha256:8a2d1c34f6b1a6e83e1bfb1e0c9a7f4d2e5b8c91d3f0a7e6c5b4a3928170605f

From that point, builds are reproducible — the tag can move upstream without silently changing what the platform builds.

Second, digest rolling. When upstream rebuilds node:20-slim with a patched Debian layer, the tag's digest moves — and Renovate opens a PR updating the pinned digest to the new one. That PR is the patch-delivery event the git-push pipeline was missing: it turns "upstream fixed a CVE in your base" into a commit, which is the one thing a git-push PaaS already knows how to deploy.

A working renovate.json for exactly this scope:

json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "enabledManagers": ["dockerfile", "docker-compose"],
  "pinDigests": true,
  "packageRules": [
    {
      "matchDatasources": ["docker"],
      "matchUpdateTypes": ["digest", "pin"],
      "groupName": "base image digests",
      "schedule": ["after 2am and before 6am every day"],
      "automerge": true
    },
    {
      "matchDatasources": ["docker"],
      "matchUpdateTypes": ["major"],
      "automerge": false
    }
  ]
}

The split is deliberate. Digest rolls under the same tag (node:20-slim → newer node:20-slim) are the same software with patched layers — safe to group, schedule nightly, and automerge. Major tag bumps (node:20node:22) change the runtime itself and stay human-reviewed. One wrinkle worth knowing: Renovate treats Docker tag suffixes like -alpine and -slim as compatibility markers, not versions, so it won't "upgrade" a tenant from slim to full-fat Debian — updates stay within the variant the tenant chose.

Making It a Platform Default, Not a Tenant Chore

Everything above is documented, public, and years old. So why is a stale base still the norm? Because as shipped, Renovate is a tenant tool: each team must know it exists, add the config, wire the bot, and tune the noise. On a self-hosted PaaS, that knowledge tax is exactly what the platform exists to absorb. Here is what the platform-default version takes.

Run Renovate against your own git server. Self-hosted Renovate runs fine as a scheduled container pointed at GitLab, Gitea/Forgejo, or GitHub — the same git server your platform already receives pushes from. Autodiscovery enumerates tenant repos; the platform ships a baseline preset (the JSON above) in its global config, so repos without their own renovate.json still get the digest loop. A repo-level config, if present, extends or overrides the preset — that's your escape hatch and customization story in one mechanism.

Give the bot a real identity and merge rights. The bot account needs API scope to open PRs and — for the automerge path — merge permission on the deploy branch. On GitLab that means Maintainer (or "Allowed to merge") on protected branches; on Gitea, a token with repo write. This is the step tenant-run setups most often fumble; a platform does it once, correctly, for everyone.

Let the merge trigger your existing deploy pipeline. This is the key design decision, and it's what makes the PaaS context special. On merge, the platform's ordinary git-push flow fires: rebuild from the now-updated Dockerfile, deploy, run the same health checks, keep the previous release for rollback. The patched base doesn't take a side door into production — it rides the exact path every other change rides, so a base image that breaks the app fails health checks and rolls back like any bad deploy. A standalone Renovate installation can't promise that; a platform that owns both the git server and the deploy pipeline can.

Make it opt-out, not opt-in. Defaults are the whole game. The tenants most exposed to stale bases are precisely the ones who will never configure a dependency bot — the unstaffed internal tool, the contractor-built service, the repo nobody has touched since the person who wrote it left. A per-app base_image_updates: off toggle respects tenant autonomy; shipping the loop dark by default just recreates the status quo.

Budget for the noise up front. Popular bases rebuild often, and digest updates are the chattiest update type. The baseline config already does the two things that matter — group all digest rolls into one PR per repo and confine runs to a nightly window. Add a registry cache or authenticated pulls so a thousand tenant repos polling Docker Hub don't hit rate limits, and surface "base image updated, redeployed, healthy" as a one-line event in the dashboard rather than a PR the tenant must babysit.

What does this buy, concretely? It changes the platform's worst-case base-image staleness from unbounded (time since the tenant's last push — months or years) to roughly one day (the nightly Renovate window plus a deploy). That single bound is the difference between "our tenants' images are patched when tenants get around to it" and "fixed upstream CVEs are off our fleet within 24 hours, by default" — a sentence a platform operator can actually say to a security review.

Where This Still Falls Short

Honesty about the limits keeps this from being a marketing pitch.

  • It's a rebuild, not a rebase. Every digest PR that merges costs a full image build — minutes of compute per service, multiplied across the fleet, versus the sub-second layer swap pack rebase gives buildpack-built images. A platform running both should use rebase where it can and the Renovate loop where it must.
  • Automerge shifts risk onto your health checks. Many tenant repos have no test suite; "CI green" means nothing there. The deploy-pipeline gating is the real safety net, and it only catches what health checks can see — a subtle behavior change from a new OS library rides straight through. Platforms should pair the default with easy per-app opt-down to "PR only, no automerge."
  • Pinning without the loop is worse than tags. A pinned digest that nobody rolls is a permanently frozen CVE set — at least a mutable tag got fresh layers on the next push. If the update automation ever stops (bot token expired, runner broke), pinned repos rot faster than unpinned ones did. Monitor the loop itself.
  • Upstream has to publish the fix. Renovate delivers whatever the image maintainer ships. An unmaintained community base image never gets a new digest; the loop can't patch what upstream never rebuilt. The platform still needs scanner-driven visibility to catch bases that are stale at the source.

None of these argue against the default. They argue for shipping it with eyes open: rebase where possible, health-gated automerge, loop monitoring, and a scanner as the backstop rather than the headline.

What a Platform Owes Its Tenants by Default

The self-hosted PaaS pitch has always been "Heroku's experience on your own machines." But Heroku's experience was never just the git push — it was the quiet, unglamorous stack maintenance underneath it, the part where the platform patched the OS under your app without asking. Tenant-authored Dockerfiles broke that contract, and most self-hosted platforms responded by silently handing the FROM line back to the tenant. The pieces to take it back — Renovate's docker datasource, digest pinning, policy-gated automerge, a deploy pipeline that already knows how to roll back — are all mature. What's missing on most platforms is only the decision to wire them together as a default.

It's also a preview of where platform automation is heading: an update loop that watches upstream, proposes a change as a commit, merges under policy, and verifies the result through health checks is agent-shaped infrastructure — the same contract an AI operator needs to safely run deploys on your behalf.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a platform contract built for both human and AI operators. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex