Skip to content

Self-hosted Mend Renovate CE — Design

Bead: hl-2ctz Date: 2026-06-14 Status: Reviewed — READY (design-reviewer, round 2)

Run Mend Renovate Community Edition (CE) as a long-running, in-cluster service that automates dependency-update PRs across multiple GitHub organizations. CE is deployed GitOps-style: an ArgoCD Application wraps the official mend-renovate-ce Helm chart, secrets come from Vault via an ExternalSecret (mirroring the fovea app), and the public webhook is exposed through the existing Cloudflare-tunnel webhook-services pattern.

The design is deliberately low-novelty: nearly every piece reuses an established cluster pattern. The only genuinely new operational property is that CE is stateful (a job queue + installation registry), which earns it a PVC.

This is not greenfield. .github/renovate.json already exists and is production-mature: every manager in use is configured (terraform, kubernetes, argocd, helm-values, kustomize, ansible-galaxy, pip/pep621), plus custom regex managers for _VERSION variables in shell/Makefiles, component-family grouped automerge (core-infra, monitoring, security, database, container images), vulnerability alerts ([SECURITY] suffix, osvVulnerabilityAlerts), a dependency dashboard, lockFileMaintenance, and global automerge with platformAutomerge. Renovate is therefore already running on this repo — almost certainly via the Mend-hosted (cloud) Renovate GitHub App.

This project is consequently a runner migration, not an adoption: move execution from the Mend cloud app to the self-hosted CE server while keeping the existing repo config essentially untouched (Renovate config is runner-agnostic — the same renovate.json works under either runner). The migration-specific concerns are captured in Decision D8, the Renovate-behavior section, and the risk table.

  • One CE instance serving multiple GitHub orgs via a single GitHub App.
  • Both trigger paths active: the embedded autodiscovery scheduler (baseline sweeps, no inbound network) and the webhook handler (instant reaction to merges, new installs, and config edits).
  • Auto-onboarding: a “Configure Renovate” PR is opened on each newly-installed repo.
  • Secrets never in Git; all credentials sourced from Vault.
  • Migrate this cluster repo (fzymgc-house/selfhosted-cluster) — which already runs Renovate via the Mend cloud app — onto the self-hosted CE runner with its existing .github/renovate.json preserved.
  • Mend Renovate Enterprise Edition features (multi-server, worker queues, Web UI, RBAC).
  • Redis cache (YAGNI at this scale; disk cache is sufficient).
  • PostgreSQL backend (see Decision D5 — SQLite chosen; PG is a documented future upgrade, not built now).
  • Platforms other than GitHub.com (GitLab / Bitbucket DC are unsupported here even though CE supports them upstream).
  • Rewriting this repo’s existing .github/renovate.json (it carries over as-is; only additive global defaults change).
  • Migrating other orgs/repos off the cloud app (this spec migrates the cluster repo; org-wide cutover is a follow-up once CE is proven).
IDDecisionRationale
D1Mend Renovate CE (not OSS-CLI, not EE)User intent; CE is the GitHub-App-driven self-hosted product. EE is overkill.
D2Deploy via official mend-renovate-ce Helm chart wrapped in an ArgoCD ApplicationMatches the cluster’s app-config convention; chart supports renovate.existingSecret + cachePersistence natively.
D3Triggering = scheduler + webhookScheduler gives baseline coverage with no exposure; webhook adds responsiveness, reusing the existing Cloudflare-tunnel webhook pattern.
D4One public GitHub App, installed per-orgCE configures exactly one App; a private App installs only on its owner, so multi-org requires a public App. One CE serves all installations.
D5SQLite on a Longhorn PVC (not CNPG Postgres)The DB is a rebuildable cache (queue + repo registry), not authoritative data. PG’s durability/HA protects data we don’t need to protect.
D6Free registered (unlimited-repo) licenseTotal repos across orgs may exceed the 10-repo unregistered cap.
D7Onboarding = auto-open onboarding PR with an org-wide base presetStandard Renovate UX; nothing updates until the onboarding PR is merged. Repos that already have a renovate.json (like this one) are not re-onboarded.
D8Migrate, don’t re-adopt: cut this repo over from the Mend cloud app to CE, preserving its existing .github/renovate.jsonConfig is runner-agnostic; the mature existing config is the proven baseline. Cutover = uninstall cloud app on the repo, install the self-hosted App.

A single CE Deployment (image ghcr.io/mend/renovate-ce, pinned tag, chart image.useFull: true for the full toolchain image) in a new renovate namespace. CE runs four logical components in one container:

  1. State store — SQLite holding the job queue + the registry of known installations/repositories.
  2. Autodiscovery schedulerMEND_RNV_CRON_APP_SYNC (default every 4h) enumerates all GitHub App installations across orgs and enqueues jobs.
  3. Webhook handler — listens on /webhook (:8080); enqueues high-priority jobs on merges, new installs, and renovate.json edits.
  4. Worker — wraps the Renovate CLI; drains the queue one repo at a time.
GitHub (multi-org App installs)
│ (a) webhook events ──► Cloudflare Tunnel ──► renovate-ce Service :8080 /webhook
│ (b) every ~4h: CE scheduler pulls installation list via GitHub API
CE job queue (SQLite on PVC)
CE worker → Renovate CLI → opens/updates/rebases PRs on each repo
  • argocd/app-configs/renovate/ — ArgoCD Application + Kustomize/Helm glue, following the existing app-config structure.
    • Application references the Mend Helm repo (https://mend.github.io/renovate-ce-ee), pins chart + appVersion, and supplies non-secret config via valuesObject.
    • external-secret.yaml — Vault-backed credentials (see Secrets).
    • networkpolicy.yaml — ingress/egress restrictions.
    • Optional ingressroute.yaml — internal /health + admin API.
  • tf/cloudflare/ — add a renovate entry to the webhook-services map.

Follows the fovea Vault→ExternalSecret pattern. The chart provides a native renovate.existingSecret knob (verified via helm show values mend-renovate-ce-ee/mend-renovate-ce), so the ExternalSecret templates a Secret whose keys are the chart’s camelCase mendRnv* names and the Application sets renovate.existingSecret: renovate-app. (This supersedes an earlier extraEnvFromSecrets/UPPERCASE approach; the implementation plan carries the exact ExternalSecret manifest.)

  • Vault path: fzymgc-house/cluster/renovate
  • Properties (secret only): mend_license_key, github_app_private_key (PEM), github_webhook_secret, server_api_secret. (App ID is non-secret — see below.)
  • ExternalSecret (ClusterSecretStore: vault, refreshPolicy: Periodic, refreshInterval: 5m — mirroring fovea’s external-secret.yaml) templates these into an Opaque Secret named renovate-app with camelCase keys:
Vault propertySecret key (for existingSecret)
mend_license_keymendRnvLicenseKey
github_app_private_keymendRnvGithubAppKey
github_webhook_secretmendRnvWebhookSecret
server_api_secretmendRnvServerApiSecret

Non-secret config (in the Application valuesObject under renovate:): mendRnvAcceptTos: "y", mendRnvPlatform: github, mendRnvGithubAppId: "<id>" (quoted — Helm coerces bare ints), mendRnvApiEnabled: "true" (mendRnvAdminApiEnabled is deprecated), mendRnvAutoDiscoverFilter: <globs>. The SQLite path is auto-set when cachePersistence.enabled: true.

A Vault policy update is required for the new fzymgc-house/cluster/renovate path (per repo rules).

Grounding (Rule 7): all MEND_RNV_* variable names above are sourced from Mend’s official CE/EE configuration reference — mend/renovate-ce-ee docs/configuration-options.md (and the GitHub setup guide) — verified during design (see bead hl-2ctz grounding/exa notes). The Mend CE repo is not indexed in DeepWiki and Context7 covers only the OSS CLI, so the implementation plan must re-verify exact names against that doc page before writing the Application manifest.

  • Single GitHub App, made public so it can be installed across multiple orgs. Installed per-org with either all-repos or a selected-repos scope.
  • Permissions (standard self-hosted Renovate App): read/write on Contents, Pull requests, Issues; read on Metadata; webhooks on push, pull_request, installation/installation_repositories.
  • Webhook URL: https://renovate-wh.<domain>/webhook, secured by the HMAC github_webhook_secret.
  • App ID + installation are non-secret (config); the private key is the only secret (Vault).
  • Public webhook: add a renovate entry to the Cloudflare-tunnel webhook-services map in tf/cloudflare/variables.tfservice_url = http://renovate-ce.renovate.svc.cluster.local (the chart’s ClusterIP Service exposes port 80 → container 8080; omit the port like the argocd entry does). DNS record is auto-created. Apply is GitOps, not manual: the main-cluster-cloudflare HCP Terraform workspace (tf/cloudflare, auto_apply = true) runs on PR merge — open the PR, review the speculative plan (production-facing module — review carefully per docs/operations/hcp-terraform.md), merge, and HCP TF applies. tf/cloudflare/MANUAL_APPLY.md is break-glass only (initial setup / HCP TF outage). GitHub POSTs /webhook, forwarded as-is. This deliberately bypasses Cloudflare Access (GitHub must reach it anonymously); HMAC is the only auth, by design.
  • NetworkPolicy:
    • Ingress: allow :8080 only from the cloudflared namespace.
    • Egress: DNS; GitHub API + git over HTTPS; package registries (npm/PyPI/etc.) as needed by enabled managers.
  • Internal access (optional): Traefik IngressRoute on renovate.fzymgc.house with router-hosts.fzymgc.house/enabled: "true" for LAN/Tailscale access to /health and the admin API.
  • Longhorn PVC mounted at the parent dir of MEND_RNV_SQLITE_FILE_PATH (e.g. /db/renovate-ce.sqlite).
  • Footgun: the container runs as uid=12021 / gid=12021 and needs rwx on the SQLite file’s parent directory. The pod must set fsGroup: 12021 (and appropriate securityContext) or CE will fail to start.
  • No Redis. Loss of the volume costs one re-sync from GitHub (cache only), not data loss.

Global Renovate CLI config via config.js mounted at /usr/src/app/config.js (or RENOVATE_* env), supplied through the chart:

  • onboarding: true + an org-wide onboardingConfig base preset (D7).
  • An extends/preset list for shared defaults across orgs.
  • MEND_RNV_AUTODISCOVER_FILTER scopes which repos CE picks up.
  • ignorePrAuthor (decision, see below) governs whether CE adopts PRs the cloud bot opened.

Existing-PR handover (resolves the bot-identity transition). By default Renovate fetches only PRs whose author matches its own bot identity (ignorePrAuthor: false). After cutover, CE’s bot identity differs from renovate[bot], so CE would not see the cloud bot’s open PRs and would open duplicates. Two valid resolutions:

  • (A) Close-and-recreate (chosen default): before/at cutover, close the cloud bot’s open Renovate PRs. CE recreates them cleanly under the new identity. Deterministic, no lingering cross-identity state. Cost: transient churn of open PRs (acceptable — they automerge anyway).
  • (B) ignorePrAuthor: true: CE ignores author and adopts existing PRs in place. Avoids churn but mixes two bot identities on one PR’s history and is harder to reason about.

This spec chooses (A); ignorePrAuthor stays false. Implementation must make this explicit so the plan author doesn’t silently inherit the default and produce duplicates.

This repo’s existing config is preserved, not rewritten. The mature .github/renovate.json already covers terraform, kubernetes, argocd, helm-values, kustomize, ansible-galaxy, pip/pep621, plus custom regex managers — it carries over verbatim under the new runner (Renovate config is runner-agnostic). The global config.js/preset must be strictly additive (org defaults) and must not override per-repo settings; per-repo renovate.json takes precedence by Renovate’s config resolution. Note the self-referential loop: CE’s own Helm chart/app version becomes a dependency that this repo’s argocd/helm-values manager will open update PRs for.

  • Health: GET /health.
  • Telemetry: container logs flow to ClickStack via the cluster’s existing OTel/Vector pipeline (no special wiring). Set LOG_FORMAT=json for clean ingestion.
  • Upgrades: chart/app version pinned in Git; Renovate will eventually open PRs to bump its own chart (self-managed).
  • Admin API: enabled (MEND_RNV_ADMIN_API_ENABLED=true, gated by server_api_secret) for probing queue state / triggering jobs from inside the cluster.
RiskMitigation
Public GitHub App is a deliberate exposureInstall-gated per org; least-privilege App permissions.
Webhook endpoint is unauthenticated-by-designHMAC github_webhook_secret; only /webhook is exposed via the tunnel.
SQLite uid/rwx startup failurefsGroup: 12021 + verified securityContext.
-full image is large; slow first pullExpected one-time cost; consider imagePullPolicy + node pre-pull if painful.
Onboarding PRs across many repos at onceOnboarding opens PRs but changes nothing until merged; autodiscoverFilter bounds initial blast radius.
GHCR image may require a pull secretVerify during implementation; add a pull secret only if the image is not public.
Dual management: cloud app + CE both acting on the repo → duplicate/conflicting PRsUninstall the Mend cloud Renovate app from the repo (and any org being cut over) before/at installing the self-hosted App. Make the cutover atomic per repo.
PR bot identity changes (renovate[bot] → self-hosted <app>[bot]) → CE can’t see cloud-bot PRs (ignorePrAuthor defaults false) → duplicate PRsChosen resolution (A): close the cloud bot’s open PRs at cutover so CE recreates under the new identity; keep ignorePrAuthor: false. (Alt (B): ignorePrAuthor: true to adopt in place.) See “Existing-PR handover” in Renovate behavior.
Automerge vs branch protection on mainThe existing config relies on platformAutomerge; the self-hosted App needs write + the repo’s required status checks must pass. Verify the App can satisfy branch protection or automerge silently stalls.

Implementation outline (for writing-plans)

Section titled “Implementation outline (for writing-plans)”
  1. Prerequisite checks (must pass before cutover): (a) Confirm the current runner is the Mend cloud app (vs. an OSS CronJob/Action) so the cutover uninstalls the right thing. (b) Re-verify all MEND_RNV_* variable names against Mend’s configuration-options.md. (c) Confirm the self-hosted App can satisfy main’s branch protection (required checks + write) so platformAutomerge won’t stall.
  2. Provision the GitHub App (public; permissions; webhook URL + secret).
  3. Obtain the free registered (unlimited) Mend license key.
  4. Store credentials in Vault fzymgc-house/cluster/renovate; update Vault policy.
  5. Create argocd/app-configs/renovate/ (Application + ExternalSecret + NetworkPolicy + PVC + optional IngressRoute), pinning chart/app versions.
  6. Add the renovate webhook-services entry in tf/cloudflare/variables.tf, open a PR, review the speculative plan, and merge — the main-cluster-cloudflare HCP TF workspace auto-applies on merge (manual apply only as break-glass).
  7. Author the additive global config.js/preset (org defaults only; keep ignorePrAuthor: false per the chosen handover; do not touch this repo’s existing .github/renovate.json).
  8. Cutover: uninstall the Mend cloud Renovate app from this repo, install the self-hosted App on it, and close any stale open renovate[bot] PRs so CE re-creates them under the new identity.
  9. Verify: pod healthy (/health), autodiscovery populates the registry, CE picks up this repo’s existing config, a fresh dependency-dashboard/PR appears under the self-hosted bot, automerge satisfies branch protection, webhook round-trips, telemetry lands in ClickStack.
  • Exact autodiscoverFilter scope per org (all vs. selected) — finalize when installing the App.
  • Whether the internal IngressRoute is wanted day-one or deferred.
  • Whether to cut the whole org over at once or repo-by-repo (this spec does this repo first; org-wide is a deliberate follow-up).

(The “current runner identity” check moved into Implementation step 0 as a hard prerequisite.)