Skip to main content

Renovate as a Kubernetes CRD: Patching Build Images Without a Hosted Bot Account

10 min readDora NodaDora Noda
Share
On this page

Somewhere in your fleet there is a FROM line nobody has touched in eleven months. Not because the image is fine — its OS packages stopped receiving security fixes two minor versions ago — but because bumping it means opening a pull request against a repo nobody owns, rebuilding an image nobody watches, and redeploying a service whose owner left the team. Multiply that by every Dockerfile, buildpack stack reference, and Helm chart image tag in the fleet, and the unpatched base image is not an edge case. It is the default state of every self-hosted platform that patches by hand.

The standard answer is a hosted dependency bot: install Mend's Renovate GitHub app or enable Dependabot, grant it repository access, and let someone else's infrastructure open the PRs. That works, right up until it doesn't fit: air-gapped or self-hosted git, compliance rules against third-party apps reading private repos, per-seat pricing on private repositories, or simply the principle that a team running its own machines would rather run its own updater. The alternative used to be a hand-rolled Renovate CronJob — a YAML manifest, a token in a Secret, and crossed fingers. In August 2026, Mogenius's Renovate Operator turned that cron job into a proper Kubernetes-native controller: repositories and schedules become custom resources, discovery and parallel execution are built in, and the whole patch loop is observable through a UI and Prometheus metrics instead of kubectl logs on a dead pod.

So here is the question this post answers concretely: can a self-hosted PaaS keep its build images patched with an in-cluster Renovate operator and no hosted dependency-bot account at all? Short answer: yes — and the design below shows the exact CRD, the dependency policy, and the guardrails that keep "automatic patching" from becoming automatic fleet-wide breakage.

From CronJob to CRD: What the Operator Changes

Renovate's own self-hosting docs still show the classic shape: a CronJob running renovate/renovate on an @hourly schedule against your git server. It works, and plenty of teams run it. But a CronJob is a blunt instrument. Every run re-discovers every repository serially. Concurrency is whatever you hand-code. There is no per-repo status object to inspect, no RBAC story beyond "whoever can edit the CronJob controls all updates," and failure visibility is a dead pod's log stream.

The operator replaces that with a declared resource. Here is the entire control plane for automated patching across an org:

yaml
apiVersion: renovate-operator.mogenius.com/v1alpha1
kind: RenovateJob
metadata:
  name: fleet-patching
  namespace: renovate-operator
spec:
  schedule: "0 2 * * *"       # nightly discovery + update run
  secretRef: renovate-secret  # platform credentials, in-cluster only
  provider:
    name: github
  image: ghcr.io/renovatebot/renovate:latest
  parallelism: 3              # at most 3 repo jobs at once
  discoveryFilters:
    - "my-org/*"

On each cron tick the operator runs a discovery job, lists the repositories matching the filters, queues them, and then reconciles the queue every ten seconds — starting a Renovate run per repo, capped by spec.parallelism. Per-project status is tracked in-cluster and visible in the built-in UI, and the controller exposes Prometheus metrics and health checks with leader election for HA. Compared against Mend's own offerings, the project's comparison table makes the pitch explicit: everything the CLI and the Community self-hosted edition do, plus a web UI, declarative scheduling, filtered auto-discovery, per-project status, and Kubernetes-native job lifecycle (TTL, deadlines, retries) — fully open source with no signup or license key.

Two details matter most for a PaaS team. First, discoveryFilters supports group and topic filtering, so platform repos, tenant build templates, and base-image repos can each get their own RenovateJob with their own schedule and parallelism budget — the team that owns the buildpacks does not share a run queue with the team that owns the docs site. Second, because the schedule and the repo set are CRDs, they are GitOps-able: the patch policy for the fleet lives in the same repo as the fleet manifests, reviewed like any other infrastructure change.

The Worked Design: A Base-Image Patch Loop

Base images are the highest-leverage Renovate target on a self-hosted PaaS. Application dependencies belong to tenants; the FROM lines in build Dockerfiles, builder images, and PaaS-managed Helm values are the platform's own attack surface, and they rot the fastest because no tenant feels responsible for them. The loop has three parts: discover the repos, pin and group the image references, and promote updates through CI before they touch production builds.

Start by giving Renovate something deterministic to update. A bare FROM node:22-slim tag floats — rebuilds silently change content — so the policy pins digests and lets the bot own the digest churn:

json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "minimumReleaseAge": "3 days",
  "packageRules": [
    {
      "description": "Pin base images to digests, group them, own cadence",
      "matchManagers": ["dockerfile"],
      "matchUpdateTypes": ["digest", "patch", "minor"],
      "pinDigests": true,
      "groupName": "base images",
      "schedule": ["before 6am on monday"],
      "automerge": true,
      "automergeType": "pr",
      "requiredStatusChecks": ["build / image", "trivy / scan"]
    },
    {
      "description": "Major base-image bumps need a human",
      "matchManagers": ["dockerfile"],
      "matchUpdateTypes": ["major"],
      "automerge": false,
      "labels": ["base-image-major", "needs-review"]
    }
  ],
  "prConcurrentLimit": 5,
  "branchConcurrentLimit": 3
}

Read the policy as a set of deliberate choices. Digest pinning plus groupName means one Monday-morning PR per repo carrying every low-risk image bump, instead of a dozen scattered PRs. minimumReleaseAge (the successor to the old stabilityDays) holds back images younger than three days, so a poisoned or yanked upstream tag never becomes your Monday. Minor and patch digest updates automerge — but only through requiredStatusChecks: the PR must survive a real image build and a Trivy scan before it lands.

Major bumps — node:22 to node:24, a buildpack stack jump — never automerge. They get labels and wait for a human who understands the breaking-change surface.

On the operator side, mirror this with per-scope RenovateJobs: a daily job with tight parallelism for the handful of base-image repos (fast feedback on the fleet's most security-sensitive lines), and a weekly, higher-parallelism job for the long tail of app repos. Discovery filters keep the two queues from competing. The result is an auditable GitOps loop: the CRD declares which repos get patched and when, renovate.json declares which updates are safe to land alone, and every exception arrives as a labeled PR rather than a silent drift.

Operator vs. Hosted Account: An Honest Comparison

Running the operator is not free — it is a controller you now operate — so the comparison with a hosted Renovate account (or Dependabot) has to be honest about both sides.

ConcernHosted bot accountIn-cluster operator
SetupInstall app, grant repo accessHelm install + Secret + CRD
Private/self-hosted gitLimited or paid tierAny Renovate-supported platform, credentials stay in-cluster
SchedulingVendor's queueCron per RenovateJob, GitOps-reviewed
Parallelism/quotasVendor-managed, opaquespec.parallelism, your own rate-limit budget
ObservabilityVendor dashboardIn-cluster UI + Prometheus, same stack as the fleet
Per-repo policyrenovate.json in repoSame renovate.json, plus CRD-level discovery filters and RBAC
Merge confidence / smart mergeMend's proprietary signalNot available; substitute minimumReleaseAge + CI gates
MaintenanceZero (vendor's problem)You upgrade the controller and the Renovate image
Cost shapePer-seat/subscription for private reposCompute you already own

The operator wins on data sovereignty (tokens never leave the cluster), on scheduling control, and on unifying patch policy with the rest of the fleet's GitOps. The hosted account wins on two things that matter: zero operational burden, and Mend's merge-confidence signal, which correlates update safety across thousands of repos — something no single fleet can reproduce. A small team with public repos and no compliance constraints should probably just install the hosted app; the operator earns its keep once the fleet has private repos on self-hosted git, regulated data, or enough repositories that vendor-queue opacity becomes its own toil.

There is also a scope question worth naming: Renovate opens PRs against source repos, which is exactly right for FROM lines and chart tags. If what you actually want is "roll the new image into the running cluster the moment the registry has it," that is a GitOps image updater's job — Flux's image-reflector and image-automation controllers, or Argo CD Image Updater — which commit tag bumps straight to the deployment repo without a PR review step. The two compose well: Renovate owns the source references (Dockerfiles, build templates) with review gates, while the image updater owns the deployed tag with automation policies. Don't force one tool to do both jobs.

Guardrails: Quota, Egress, Credentials, Blast Radius

"Automatic patching" without guardrails is a fleet-wide incident waiting for a Monday morning. Each of the four failure domains below needs an explicit control, configured before the first automerge lands.

Quota: bound the bot's appetite for API calls. Renovate is chatty — discovery, lookups, PR creation — and git providers rate-limit aggressively. The operator's spec.parallelism caps concurrent repo jobs, but also set Renovate-side limits (prConcurrentLimit, branchConcurrentLimit) so one noisy org can't open forty PRs in an hour, and stagger RenovateJob schedules so discovery for different scopes doesn't stampede the provider at the same minute. Watch the rate-limit headers in the controller metrics during the first week and tune down until the ceiling stops appearing.

Egress: know where lookups go. Every update check is an outbound call to a registry or package index — Docker Hub, ghcr.io, npm, PyPI. On metered or filtered egress (and Docker Hub's pull rate limits apply to manifest lookups too), mirror aggressively: point image lookups at a registry mirror or pull-through cache you control, and keep an allowlist of registries the Renovate jobs may reach. A patch bot that can't reach an index fails safe (no PR), but a patch bot hammering a rate-limited registry can starve real builds sharing the same NAT IP.

Credentials: least privilege, in-cluster only. The operator reads platform tokens from a Kubernetes Secret referenced by secretRef. Scope that token to exactly what patching needs — repo read/write for PRs, nothing admin — and prefer a dedicated bot account or fine-grained PAT over a human's token, so rotation doesn't break someone's workflow and a leak doesn't inherit someone's org-owner rights. Registry credentials for private base images belong in self-hosted hostRules in the operator's config, never in a repo's renovate.json, where any contributor could read them. And because the schedule is a CRD, use Kubernetes RBAC to decide who may create or relax a RenovateJob — the ability to set parallelism: 50 or widen a discovery filter to */* is production access by another name.

Blast radius: never let one bad update fan out. This is the layer that turns automation from frightening to boring. Concretely: group low-risk updates so each PR is reviewable as a unit; hold majors for humans; require green build-plus-scan checks before any automerge; roll the merged base image through a staging build that exercises the PaaS's own deploy path before promoting it to the default builder tenants inherit; and keep exactly one base-image lineage change in flight per repo (branchConcurrentLimit exists for this). The failure mode you're defending against is specific and historical: an upstream image ships a broken or malicious tag, every fleet repo bumps to it overnight, and morning brings a fleet that can't build. Digest pinning plus minimumReleaseAge plus staged promotion turns that from a fleet-wide event into one quarantined PR.

What This Means for a Self-Hosted PaaS

The deeper point is architectural. A PaaS that provisions machines declaratively but patches its build inputs by hand has a hole in the middle of its GitOps story: the fleet converges to the declared state while the declared state's own ingredients quietly go stale. Putting the updater inside the cluster — scheduled by CRD, filtered by scope, gated by CI — closes that hole with the same primitives the platform already trusts: manifests in git, controllers reconciling toward them, and RBAC deciding who may change what.

Start small: one RenovateJob over the base-image repos, digest pinning on, majors held for review, automerge gated on build-plus-scan. Measure the Monday PRs for a month. Then widen the discovery filters, scope by scope, until the fleet's FROM lines are the freshest part of the codebase instead of the stalest.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. 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