# ============================================ # Renovate Bot - Self-hosted dependency updater # ============================================ # https://docs.renovatebot.com # Image: renovate/renovate (plain tag = minimal image, "formerly slim"; # -slim suffix was retired after v37.440.x, so we pin the plain tag) # # PILOT SCOPE (intentionally narrow): # Runs weekly (Sat 02:00 Europe/Budapest) as a CronJob and opens # dependency-update PRs against admin/homelab-manifests on Gitea. # Only the `kubernetes` and `helm-values` managers are enabled, and a # default-deny packageRule limits updates to exactly four pilot images: # - ghcr.io/thomiceli/opengist # - louislam/uptime-kuma # - f0rc3/gokapi # - docker.io/calcom/cal.com # minor/patch -> PR with Gitea native auto-merge; major -> waits for # manual approval via a checkbox on the Dependency Dashboard issue. # # Stateless & ephemeral: no Service, Ingress, or PVC. Writable /tmp is an # emptyDir (root FS is read-only); Renovate uses it for git clones + cache. # # Secrets (created manually, NOT in git) come from Secret `renovate-secrets`: # - RENOVATE_TOKEN (Gitea PAT) # - RENOVATE_GITHUB_COM_TOKEN (GitHub PAT, for release notes) # ============================================ --- apiVersion: v1 kind: ConfigMap metadata: name: renovate-config namespace: admin-system labels: app.kubernetes.io/instance: renovate app.kubernetes.io/name: renovate data: config.json: | { "platform": "gitea", "endpoint": "https://gitea.dooplex.hu/api/v1", "gitAuthor": "Renovate Bot ", "repositories": ["admin/homelab-manifests"], "onboarding": false, "requireConfig": "optional", "dependencyDashboard": true, "dependencyDashboardTitle": "Renovate Dependency Dashboard", "prHourlyLimit": 16, "prConcurrentLimit": 16, "enabledManagers": ["kubernetes", "helm-values", "custom.regex"], "kubernetes": { "managerFilePatterns": ["/.+\\.ya?ml$/"] }, "customManagers": [ { "description": "termix: docker image tag is `release-X.Y.Z` but the upstream GitHub release tag_name is `release-X.Y.Z-tag` (different from the release name). regex versioning parses currentValue (no -tag); extractVersion strips the -tag suffix from candidate tag_names so they normalize to the same shape Renovate writes back to the manifest.", "customType": "regex", "managerFilePatterns": ["/termix-system/.+\\.ya?ml$/"], "matchStrings": [ "image:\\s+(?ghcr\\.io/lukegus/termix):(?release-\\d+\\.\\d+\\.\\d+)" ], "datasourceTemplate": "github-releases", "packageNameTemplate": "Termix-SSH/Termix", "versioningTemplate": "regex:^release-(?\\d+)\\.(?\\d+)\\.(?\\d+)$", "extractVersionTemplate": "^(?release-\\d+\\.\\d+\\.\\d+)" }, { "description": "linuxserver servarr apps (prowlarr, radarr, sonarr) use tag pattern `version-X.Y.Z.B` (4 segments + `version-` prefix). The kubernetes manager's default docker versioning rejects them at the pre-check (same failure class as termix), so no PRs ever open. Use regex versioning to parse the prefixed 4-segment form; depName is captured from the regex so the same customManager handles all three apps.", "customType": "regex", "managerFilePatterns": ["/servarr-system/.+\\.ya?ml$/"], "matchStrings": [ "image:\\s+linuxserver/(?prowlarr|radarr|sonarr):(?version-\\d+\\.\\d+\\.\\d+\\.\\d+)" ], "datasourceTemplate": "docker", "packageNameTemplate": "linuxserver/{{depName}}", "versioningTemplate": "regex:^version-(?\\d+)\\.(?\\d+)\\.(?\\d+)\\.(?\\d+)$" } ], "packageRules": [ { "description": "All apps: 3-day stability gate before any PR opens", "matchPackageNames": ["*"], "minimumReleaseAge": "3 days" }, { "description": "Auto-merge minor/patch after the stability window", "matchUpdateTypes": ["minor", "patch"], "automerge": true, "automergeType": "pr", "platformAutomerge": true }, { "description": "Major bumps wait for dashboard approval (catches breaking/schema migrations)", "matchUpdateTypes": ["major"], "automerge": false, "dependencyDashboardApproval": true }, { "description": "k3s-bundled components: never touch, they ride k3s upgrades", "matchPackageNames": [ "rancher/local-path-provisioner", "rancher/mirrored-coredns/coredns", "rancher/mirrored-metrics-server" ], "enabled": false }, { "description": "Critical core: PR opens with changelog but Viktor merges manually (deploy pipeline + SSO + DB operator). Some entries are no-ops if the image isn't pinned in this repo (ArgoCD bootstrap, authentik outpost images inherit chart defaults).", "matchPackageNames": [ "gitea/gitea", "quay.io/argoproj/argocd", "ghcr.io/goauthentik/server", "ghcr.io/goauthentik/ldap", "ghcr.io/goauthentik/proxy", "ghcr.io/cloudnative-pg/cloudnative-pg" ], "automerge": false }, { "description": "wanderer: db + web update together in one PR", "matchPackageNames": ["flomp/wanderer-db", "flomp/wanderer-web"], "groupName": "wanderer" }, { "description": "meilisearch: every version bump can require an index format migration via dump/restore (see https://www.meilisearch.com/docs/learn/update_and_migration/updating). PR #32 (v1.11.3 -> v1.45.2) on 2026-06-06 broke wanderer with `Your database version (1.11.3) is incompatible with your current engine version (1.45.2)`. Hold ALL meilisearch updates behind dashboard approval so the migration is planned before the PR even opens.", "matchPackageNames": ["getmeili/meilisearch"], "dependencyDashboardApproval": true }, { "description": "Postgres-family images: a major bump (e.g. 16 -> 17) requires pg_upgrade or dump/restore — the new server binary refuses to open the old data directory (`database files are incompatible with server`). PR #76 (immich-app/postgres 16 -> 17) on 2026-06-06 crashlooped immich-postgres and immich-server. Renovate's docker versioning treats these custom tag formats inconsistently, so don't trust the major/minor classification: hold ALL updates for these images behind explicit dashboard approval. Includes vanilla postgres, postgis/postgis (where the tag prefix IS the pg major), and ghcr.io/immich-app/postgres (custom `N-vectorchordX.Y.Z` form).", "matchPackageNames": [ "postgres", "postgis/postgis", "ghcr.io/immich-app/postgres" ], "dependencyDashboardApproval": true }, { "description": "termix: kubernetes manager would extract the image with versioning=docker and silently skip it (release-1.11.0 fails the docker pre-check). Disable that extraction; customManagers above does the real work via github-releases.", "matchManagers": ["kubernetes"], "matchPackageNames": ["ghcr.io/lukegus/termix"], "enabled": false }, { "description": "linuxserver servarr apps: same disable pattern as termix. The customManager above handles extraction with the right versioning; turn off the default kubernetes-manager extraction so it doesn't silently skip + clutter the dashboard.", "matchManagers": ["kubernetes"], "matchPackageNames": [ "linuxserver/prowlarr", "linuxserver/radarr", "linuxserver/sonarr" ], "enabled": false } ], "labels": ["renovate"] } --- apiVersion: batch/v1 kind: CronJob metadata: name: renovate namespace: admin-system labels: app.kubernetes.io/instance: renovate app.kubernetes.io/name: renovate app.kubernetes.io/version: "43.209.3" spec: # Sat 02:00 Europe/Budapest — leaves the full weekend for troubleshooting # if a Renovate-merged update breaks something. schedule: "0 2 * * 6" timeZone: "Europe/Budapest" concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 startingDeadlineSeconds: 600 jobTemplate: metadata: labels: app.kubernetes.io/instance: renovate app.kubernetes.io/name: renovate app.kubernetes.io/version: "43.209.3" spec: template: metadata: labels: app.kubernetes.io/instance: renovate app.kubernetes.io/name: renovate app.kubernetes.io/version: "43.209.3" annotations: # Renovate uses plain X.Y.Z semver tags (no -slim suffix anymore) match-regex.version-checker.io/renovate: '^\d+\.\d+\.\d+$' spec: enableServiceLinks: false restartPolicy: OnFailure containers: - name: renovate image: renovate/renovate:43.209.3 imagePullPolicy: IfNotPresent envFrom: - secretRef: name: renovate-secrets env: - name: TZ value: Europe/Budapest - name: LOG_LEVEL value: info - name: RENOVATE_CONFIG_FILE value: /config/config.json # Renovate needs a writable tmp for git clones + cache; # root FS is read-only so point it at the emptyDir below. - name: TMPDIR value: /tmp resources: requests: cpu: 100m memory: 256Mi limits: cpu: 2000m memory: 2Gi securityContext: runAsNonRoot: true runAsUser: 12021 runAsGroup: 0 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL volumeMounts: - name: config mountPath: /config readOnly: true - name: tmp mountPath: /tmp volumes: - name: config configMap: name: renovate-config - name: tmp emptyDir: sizeLimit: 2Gi