Self-hosted Mend Renovate CE — Design
Bead: hl-2ctz Date: 2026-06-14 Status: Reviewed — READY (design-reviewer, round 2)
Summary
Section titled “Summary”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.
Current state & migration framing
Section titled “Current state & migration framing”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.jsonpreserved.
Non-goals / Out of scope
Section titled “Non-goals / Out of scope”- 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).
Key decisions
Section titled “Key decisions”| ID | Decision | Rationale |
|---|---|---|
| D1 | Mend Renovate CE (not OSS-CLI, not EE) | User intent; CE is the GitHub-App-driven self-hosted product. EE is overkill. |
| D2 | Deploy via official mend-renovate-ce Helm chart wrapped in an ArgoCD Application | Matches the cluster’s app-config convention; chart supports renovate.existingSecret + cachePersistence natively. |
| D3 | Triggering = scheduler + webhook | Scheduler gives baseline coverage with no exposure; webhook adds responsiveness, reusing the existing Cloudflare-tunnel webhook pattern. |
| D4 | One public GitHub App, installed per-org | CE configures exactly one App; a private App installs only on its owner, so multi-org requires a public App. One CE serves all installations. |
| D5 | SQLite 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. |
| D6 | Free registered (unlimited-repo) license | Total repos across orgs may exceed the 10-repo unregistered cap. |
| D7 | Onboarding = auto-open onboarding PR with an org-wide base preset | Standard Renovate UX; nothing updates until the onboarding PR is merged. Repos that already have a renovate.json (like this one) are not re-onboarded. |
| D8 | Migrate, don’t re-adopt: cut this repo over from the Mend cloud app to CE, preserving its existing .github/renovate.json | Config is runner-agnostic; the mature existing config is the proven baseline. Cutover = uninstall cloud app on the repo, install the self-hosted App. |
Architecture
Section titled “Architecture”Components
Section titled “Components”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:
- State store — SQLite holding the job queue + the registry of known installations/repositories.
- Autodiscovery scheduler —
MEND_RNV_CRON_APP_SYNC(default every 4h) enumerates all GitHub App installations across orgs and enqueues jobs. - Webhook handler — listens on
/webhook(:8080); enqueues high-priority jobs on merges, new installs, andrenovate.jsonedits. - Worker — wraps the Renovate CLI; drains the queue one repo at a time.
Data flow
Section titled “Data flow”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 repoDeployment layout
Section titled “Deployment layout”argocd/app-configs/renovate/— ArgoCDApplication+ Kustomize/Helm glue, following the existing app-config structure.Applicationreferences the Mend Helm repo (https://mend.github.io/renovate-ce-ee), pins chart + appVersion, and supplies non-secret config viavaluesObject.external-secret.yaml— Vault-backed credentials (see Secrets).networkpolicy.yaml— ingress/egress restrictions.- Optional
ingressroute.yaml— internal/health+ admin API.
tf/cloudflare/— add arenovateentry to the webhook-services map.
Secrets & Vault
Section titled “Secrets & Vault”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— mirroringfovea’sexternal-secret.yaml) templates these into an Opaque Secret namedrenovate-appwith camelCase keys:
| Vault property | Secret key (for existingSecret) |
|---|---|
mend_license_key | mendRnvLicenseKey |
github_app_private_key | mendRnvGithubAppKey |
github_webhook_secret | mendRnvWebhookSecret |
server_api_secret | mendRnvServerApiSecret |
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-eedocs/configuration-options.md(and the GitHub setup guide) — verified during design (see beadhl-2ctzgrounding/exanotes). 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.
GitHub App
Section titled “GitHub App”- 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 HMACgithub_webhook_secret. - App ID + installation are non-secret (config); the private key is the only secret (Vault).
Networking
Section titled “Networking”- Public webhook: add a
renovateentry to the Cloudflare-tunnelwebhook-servicesmap intf/cloudflare/variables.tf→service_url = http://renovate-ce.renovate.svc.cluster.local(the chart’s ClusterIP Service exposes port 80 → container 8080; omit the port like theargocdentry does). DNS record is auto-created. Apply is GitOps, not manual: themain-cluster-cloudflareHCP Terraform workspace (tf/cloudflare,auto_apply = true) runs on PR merge — open the PR, review the speculative plan (production-facing module — review carefully perdocs/operations/hcp-terraform.md), merge, and HCP TF applies.tf/cloudflare/MANUAL_APPLY.mdis 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
:8080only from the cloudflared namespace. - Egress: DNS; GitHub API + git over HTTPS; package registries (npm/PyPI/etc.) as needed by enabled managers.
- Ingress: allow
- Internal access (optional): Traefik
IngressRouteonrenovate.fzymgc.housewithrouter-hosts.fzymgc.house/enabled: "true"for LAN/Tailscale access to/healthand the admin API.
State / persistence
Section titled “State / persistence”- 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=12021and needsrwxon the SQLite file’s parent directory. The pod must setfsGroup: 12021(and appropriatesecurityContext) or CE will fail to start. - No Redis. Loss of the volume costs one re-sync from GitHub (cache only), not data loss.
Renovate behavior (global config)
Section titled “Renovate behavior (global config)”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-wideonboardingConfigbase preset (D7).- An extends/preset list for shared defaults across orgs.
MEND_RNV_AUTODISCOVER_FILTERscopes 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.
Operations
Section titled “Operations”- Health:
GET /health. - Telemetry: container logs flow to ClickStack via the cluster’s existing
OTel/Vector pipeline (no special wiring). Set
LOG_FORMAT=jsonfor 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 byserver_api_secret) for probing queue state / triggering jobs from inside the cluster.
Risks & mitigations
Section titled “Risks & mitigations”| Risk | Mitigation |
|---|---|
| Public GitHub App is a deliberate exposure | Install-gated per org; least-privilege App permissions. |
| Webhook endpoint is unauthenticated-by-design | HMAC github_webhook_secret; only /webhook is exposed via the tunnel. |
| SQLite uid/rwx startup failure | fsGroup: 12021 + verified securityContext. |
-full image is large; slow first pull | Expected one-time cost; consider imagePullPolicy + node pre-pull if painful. |
| Onboarding PRs across many repos at once | Onboarding opens PRs but changes nothing until merged; autodiscoverFilter bounds initial blast radius. |
| GHCR image may require a pull secret | Verify 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 PRs | Uninstall 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 PRs | Chosen 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 main | The 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)”- 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’sconfiguration-options.md. (c) Confirm the self-hosted App can satisfymain’s branch protection (required checks + write) soplatformAutomergewon’t stall. - Provision the GitHub App (public; permissions; webhook URL + secret).
- Obtain the free registered (unlimited) Mend license key.
- Store credentials in Vault
fzymgc-house/cluster/renovate; update Vault policy. - Create
argocd/app-configs/renovate/(Application + ExternalSecret + NetworkPolicy + PVC + optional IngressRoute), pinning chart/app versions. - Add the
renovatewebhook-services entry intf/cloudflare/variables.tf, open a PR, review the speculative plan, and merge — themain-cluster-cloudflareHCP TF workspace auto-applies on merge (manual apply only as break-glass). - Author the additive global
config.js/preset (org defaults only; keepignorePrAuthor: falseper the chosen handover; do not touch this repo’s existing.github/renovate.json). - 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. - 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.
Open questions
Section titled “Open questions”- Exact
autodiscoverFilterscope 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.)