Repository navigation
feat(kubernetes): add the libredb-platform Helm chart - #23
Merged
Merged
Conversation
Install LibreDB Platform on Kubernetes with Helm. The chart is the installer's Kubernetes mode driven by Helm: a bootstrap hook and jobs run the installer's own code, bundled into the chart, so a Helm install generates the same secrets, CA, PostgreSQL roles, Keycloak realm and app environments as a Docker install, with Studio as a pinned dependency. The chart covers managed or external PostgreSQL and Keycloak, generated or site TLS, upgrades, rollbacks and refused downgrades, a restart job for changed inputs, Argo CD, OpenShift's restricted-v2 SCC, seal key and secret rotation through values, amd64 scheduling for the Platform image, backup and restore, and an install without network access. Tests: unit tests in installer/test/unit/kubernetes hold the rendered chart to the installer, and the scripts in charts/libredb-platform/tests/e2e drive kind, Argo CD and OpenShift with a browser suite in installer/e2e/kubernetes. Docs: docs/deploy/install-kubernetes.md and the chart README.
…etwork policies Platform's sign-in callback sets its session cookie in chunks that hold the Keycloak ID and refresh tokens, more response header than the 4k that ingress-nginx buffers by default, so the first sign-in of a chart behind ingress-nginx ended in a 502 (upstream sent too big header). The Ingresses now carry nginx.ingress.kubernetes.io/proxy-buffer-size: 16k, which other controllers ignore. The browser suite's first test passed on that 502, because the callback URL is not /login; it now asks /api/auth/session for the signed-in admin, as the Docker suite does. The seal-envelopes and seal-canary init containers ran their script with one psql call. A network policy controller such as k3s' kube-router refuses a new pod's first connection until it holds the pod's address, so every bootstrap pod of a retirement failed at once and the upgrade hung until its timeout. The containers now wait up to 30 s for a connection, and still run the script once, so a refusal stays the job's failure. Found on k3s v1.36 behind Dokploy (Traefik) and CapRover (ingress-nginx); run-e2e.sh, run-rotation.sh, run-restore.sh and run-argocd.sh pass there with these changes.
…ncy build helm dependency build takes the Studio chart that Chart.lock pins only from a repository Helm knows, so on a fresh machine the guide's first command stopped with "no repository definition for https://libredb.org/libredb-studio/". CI never ran it, because it copies the chart that embed downloaded. The guide, the chart README and run-e2e.sh now run helm repo add first. The guide also names ingress-nginx and the 16 KB of response headers an ingress controller must accept, the chart README lists the annotation's default, and the section on an install without network access gets its missing heading.
…or CapRover Both PaaS hold ports 80 and 443, so LibreDB installs there in tls.mode external behind their proxy. Tested on Dokploy 0.30.10 and CapRover 1.15.4, which found: - An nginx with its defaults, such as CapRover's, answers Platform's sign-in callback 502 while every verify check passes; the README now asks for 16 KB of response headers and gives the nginx setting. - A DOCKER-USER rule on port 8080 alone also drops the apps' calls to Keycloak on its port 8080; the README gives a rule that matches the external interface. - The proxy arrives from the gateway of libredb_edge, which Docker picks only when the first apply creates the network; the README says how to read it, and studio-oidc-chain now names trustedProxies when Studio sends its callback with http:. - Dokploy's daily cleanup and CapRover after every deploy prune stopped containers, after which "docker compose start studio" starts nothing and exits 0, and Dokploy's cleanup of unused volumes deletes their volumes. The kill switch runbook now starts Studio with up -d, which the runbooks drill checks after pruning the stopped container, and the README describes both cleanups.
cevheri
approved these changes
Oct 23, 2026
Main changed the installer's Keycloak and platform.env code (#34), so the committed bundle no longer matched; regenerated with bun run build:helm-jobs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
LibreDB Platform installs on Kubernetes with Helm.
charts/libredb-platformis the installer's Kubernetes mode driven by Helm: a bootstrap hook and the chart's jobs run the installer's own code, bundled into the chart asfiles/libredb-jobs.cjs.gz, so a Helm install generates and converges the same secret store, internal CA, PostgreSQL roles, Keycloak realm and app environments as a Docker install. The Studio chart comes in as a pinned dependency with governance on.Beyond a first install, the chart covers upgrades with refused downgrades, Argo CD, OpenShift's restricted-v2 SCC, seal key and secret rotation through values, backup and restore, and an install without network access. Each of these was driven end to end on a real cluster; the testing section lists what each run did and measured.
The largest problem the tests found was in seal key rotation: the bootstrap job writes Studio's keyring from the secret store on every upgrade, so a key added by hand, as the runtime guide described, would have been removed by the next
helm upgrade. The chart now drives the installer's own rotation throughplatform.sealKeys, with the same refusals aslibredb-installer apply.Type of Change
Related Issue
No issue was opened; this delivers the "Kubernetes" item of ROADMAP.md.
Changes Made
The chart (
charts/libredb-platform)templates/bootstrap-hook.yaml: the bootstrap Job as a pre-install, pre-upgrade and pre-rollback hook (Argo CD PreSync), with a ServiceAccount and Role that may create Secrets and ConfigMaps and replace only the installation's own. It renders the installation document from the values, converges the secret store and the internal CA, writes every Secret and ConfigMap the workloads read, records which workloads must restart, and refuses a downgrade or an immutable change. Exit codes 2, 3 and 6 fail the Job at once throughpodFailurePolicy, so a refusal takes seconds instead of the retries.templates/postgres.yaml: managed PostgreSQL 18 with verified TLS from the internal CA and apg_hba.confthat accepts onlyhostsslwith SCRAM, and the provisioning job that runs the installer's provisioning script. Withpostgresql.mode: external, a DBA runs the script the chart writes to Secretlibredb-postgres-dba-script.templates/keycloak.yaml: managed Keycloak behind the installer's Caddy edge, which forwards only the realm's public paths; the Admin API stays in the cluster.templates/jobs.yaml: the migrate and keycloak-configure jobs, named after the revision and what they run (Argo CD Sync hooks), and the restart job (post-install, post-upgrade, post-rollback, Argo CD PostSync), which restarts only the workloads whose inputs changed and then records the release as the one the installation runs.templates/platform.yaml,templates/networkpolicy.yaml,templates/NOTES.txt,values.yaml,Chart.yaml,Chart.lock,README.md: Platform with its Ingress and upload volume, NetworkPolicies for PostgreSQL and Keycloak's Admin API, and every value documented.templates/_helpers.tpl: render guards that name the value when a required value is missing or two copies of a host or the issuer disagree.NET_BIND_SERVICE, which its binary carries.OpenShift
global.compatibility.openshift.adaptSecurityContext(auto,force,disabled), the value the Studio chart already reads, leaves out the pods' fixedrunAsUser,runAsGroupandfsGroupwhen the API server servessecurity.openshift.io/v1, so restricted-v2 assigns them from the namespace's range. Off OpenShift the rendered chart is byte for byte what it was without the value.Seal key and secret rotation
platform.sealKeys.issueNew,activeKidandretirebecomebridge.sealKeysof the installation document, onehelm upgradeper step of the seal key runbook.installer/src/kubernetes/bootstrap.ts:rotateSealKeysapplies the installer'splanSealRotationandsealKidsToRetire: a new live key, a retired key kept in the store, a refusal with exit 6 for the active kid, an unknown kid, or a kid a connection still uses.seal-envelopesinit container runs the installer'senvelopeUsesScriptaslibredb_platform; after the release applied, the restart job'sseal-canaryinit container runsdeleteCanaryScriptfor each retired kid. A unit test holds the chart's SQL to the installer's text, and a kid that is not 16 hex digits fails the render.platform.authSecretPrevious.existingSecretmounts the previous PlatformAUTH_SECRETasplatform.authSecretPreviousFile, which the installer already supported.libredb-secretsand upgrading.Scheduling
kubernetes.io/arch: amd64unlessplatform.nodeSelectornames another arch, and followplatform.nodeSelectorandplatform.tolerations.Installer
src/kubernetes/(bootstrap, keycloak job, names),src/adapters/kubernetes-api.ts(the in-cluster API client),src/cli/kubernetes-job.ts(the jobs' entry point and exit codes), andscripts/build-helm-jobs.ts(bun run build:helm-jobs), which writes the gzipped bundle the chart mounts.e2e/helpers/studio.tsonly reads an optionalLIBREDB_E2E_STUDIO_URL, with the same default.package.json: thebuild:helm-jobsscript.CI
.github/workflows/installer.ymlalso runs oncharts/**and lints the chart withhelm lint --strict, with the setup-helm action pinned by SHA..gitignore: the chart's downloaded dependency and the test output.Docs
docs/deploy/install-kubernetes.md: requirements, values, install, license, CA, first sign-in, back up and restore, external PostgreSQL and Keycloak, upgrade (and the two Helm behaviours to know when something goes back), rotate secrets and the seal key, differences from Docker, security notes, Argo CD, OpenShift, without network access, and the end-to-end tests.docs/deploy/install-docker.md,docs/deploy/platform-runtime.mdandinstaller/README.md.Testing
Gates
bun run coverage:check),bun run build,bun run release:check, the contracts tests against the pinned app images (5/5).helm lint charts/libredb-platform --strictwith the values of the CI job.New unit tests (
installer/test/unit)kubernetes/chart.test.ts(59): renders the chart withhelm templateand holds it to the installer: render guards, the installation document against the schema for every mode, the release manifest's image and chart pins, Studio's and Keycloak's environments against the installer's renderers, the bootstrap Role against every object the job writes, the pods' security contexts, OpenShift adaptation in its three modes, amd64 scheduling, the rotation values, the init containers' SQL againstenvelopeUsesScriptanddeleteCanaryScript, the init containers run against a psql that refuses its first connections (they wait, and run the script once), the ingress-nginx buffer annotation, theAUTH_SECRETmount.kubernetes/bootstrap.test.ts(34): first install, external PostgreSQL and Keycloak, upgrades that change only what changed, restart requests, downgrade refusal, the seal key runbook step by step, the exit 6 refusals and their unchanged store.kubernetes/keycloak.test.ts(9),adapters/kubernetes-api.test.ts(12),kubernetes/job-exit.test.ts(1), andkubernetes/bundle.test.ts(1), which fails when the committed bundle is stale.End-to-end runs
Each script is under
charts/libredb-platform/tests/e2e/and documented in its header. The browser suite (installer/e2e/kubernetes/helm-install.k8s.ts) reuses the Docker suite's helpers: the first admin's first sign-in with a new password and TOTP, the license page, a connection to a target PostgreSQL with its credentials sealed to Studio's key, a read grant to a Keycloak user, the user reading the table in Studio and failing to write, the revocation reaching Studio, and the user's statement in Platform's audit log.run-e2e.sh(kind, namespace enforcing the restricted Pod Security Standard): install in 63 to 70 s; Platform, Studio and the realm answer over TLS from the internal CA; the edge answers 404 for/admin/and/realms/master/and 400 for/realms/libredb/..;/master/; a pod of another namespace reaches Platform but not Keycloak's Admin API or PostgreSQL; license active; Studio's seal canaryok; browser suite 7/7 with the revocation reaching Studio in 262 to 942 ms; an upgrade with a changed value keeps the seal keys and the CA and reaches the running Platform; a rollback restores the earlier value; uninstall keeps the Secrets and volumes, and a reinstall reuses the seal keys, the CA, the database and the realm.run-external.sh(kind): a PostgreSQL of a simulated customer with its own CA, provisioned with the DBA script, the site's Keycloak configured by the chart's job with an admin user, the customer's TLS Secrets and a license Secret; endpoints, license and canary as above.run-lifecycle.sh(kind): PostgreSQL and ingress leaves set to expire in 9 days are renewed by one upgrade (824 days left), PostgreSQL restarts on its own, Platform and Studio stay ready; licenses expiring, expired and active again each reach the running Platform with an upgrade.run-upgrade.sh(kind): with a 1.0.2 build of this branch, an upgrade that fails after its hooks under--atomicrolls back cleanly to 1.0.1, which the installation still counts as its own; the upgrade to 1.0.2 runs the migrate job and Platform on 1.0.2 and keeps the seal keys and the CA; an upgrade back to 1.0.1 andhelm rollbackto the 1.0.1 revision are both refused at once with Platform still on 1.0.2; the documented recovery from the refused rollback'spending-rollbackrecord lets the next upgrade through.run-argocd.sh(kind, Argo CD 3.5.5 with label resource tracking, the namespace enforcing restricted): the packaged chart served from a Helm repository in the cluster syncs to Synced and Healthy in 99 s with automated prune and self-heal; the jobs' Secrets carry no tracking label andPrune=false; a new sync with prune runs the jobs again and keeps every Secret and the seal keys; a changed value reaches the running Platform through the PostSync restart job.run-openshift.sh(OpenShift 4.21.30, Kubernetes 1.34.9, on AWS, through a Red Hat Developer Sandbox: one namespace, no cluster rights): install in 158 to 162 s; every pod under restricted-v2 with the namespace's uid (1002000000); edge Routes made from the Ingresses; endpoints, edge rules, license and canary; browser suite 7/7; upgrade, rollback, uninstall and reinstall as inrun-e2e.sh. Run before and after the rotation and scheduling changes.run-rotation.sh(kind, browser driven, the granted user querying between every step): a new key reaches every Studio heartbeat while Platform keeps the old one; the same values again issue no second key; Platform switches withactiveKidand its canary turnsok; retiring the old key before the re-entry fails the upgrade at once with the bootstrap log naming the connection, and nothing changes; the admin re-enters the credentials in the edit form after Platform's warning; the retirement then takes the key out of every keyring and its canary row out of Platform's table, and the store keeps it asretired. Then Studio's JWT secret (every Studio session refused, the user signed in again through the live Keycloak session without a password), Platform'sAUTH_SECRETwith its previous value (the admin's session survives), Studio's Keycloak client secret andlibredb_platform's password (a new sign-in and Platform's reads work). 4/4 in 6.3 minutes.run-restore.sh(kind): a connection, a grant and a read in Studio; the backup set of the guide (the three Secrets and apg_dump -Fcoflibredbandkeycloak); the namespace deleted with its volumes; the set restored into a new namespace of the same name; the seal keys and the CA are the backup's, the license is active and the canaryok; the first admin signs in with the password and TOTP of the backup, Platform holds the connection with its envelope unchanged byte for byte, and the user reads the table through the credential sealed before the backup.run-resilience.sh(kind): after a seed like the restore test, PostgreSQL killed with no grace period (every endpoint back in 5 s), Platform, Studio and Keycloak killed at once (66 s, Keycloak's start being the longest), the node container restarted as a power cut (97 s); after each, the connections with their envelopes, the grants and the realm's users fingerprint the same; the final browser check passes.run-offline.sh(kind with a local registry):helm package --dependency-updateholds the Studio chart; the five images copied to the mirror by digest; the node's copies removed; Helm run with no repository and every proxy pointing at a closed port; installed from the archive in 81 s, every container's image ID from the mirror; endpoints, license and canary as above.run-soak.sh(kind): one installation left running, endpoints checked every 30 s, each container's memory and CPU, the restart counts and the databases' and logs' sizes every 5 minutes, and the browser check every 30 minutes. A 48-hour run was stopped at 32 hours; its clean first 28.7 hours: 10179/10179 checks answered 200 (median 10 to 14 ms, slowest 21 ms), 60/60 browser checks, no restart; memory after the first 12 hours, in MiB per hour: Platform +0.35 (206 to 220 MiB), Studio -0.04 (114 to 119), PostgreSQL -0.39, Keycloak +0.98 and slowing. An unattended Docker upgrade on the host restarted the node at hour 29; every pod came back on its own in 72 s.Recreatefor the single-replica Platform and Keycloak) or false positives (GOVERNANCE_PRIVATE_KEYS_FILEis a path). trivy 0.76.0 on the five images: see Additional Notes.run-e2e.sh,run-external.sh,run-lifecycle.sh,run-upgrade.sh(with a 1.0.2 build of the final branch),run-argocd.sh,run-restore.sh,run-rotation.sh,run-resilience.sh,run-soak.shandrun-openshift.shall ran again and passed;run-offline.shran before that change.Follow-up: Dokploy and CapRover (commits fc8e5d3, b11f365, 2c594ad)
Each in a fresh Ubuntu 24.04 KVM VM (8 vCPU, 14 GB, Docker 29.9.2), with a site CA and leaf for the three
.libredb.testhosts and a resolver that points them at the VM, as a customer's DNS would. Docker mode ran first, then the chart on k3s v1.36.5+k3s1 on the same host behind the same PaaS.preflightwith the defaults refuses port 443, namingdokploy-traefik; withtls.mode: externalbehind Dokploy's Traefik (a dynamic file router and certificate,trustedProxiesthe gateway oflibredb_edge),applyin 2 min 38 s, 19 checks passed, and the installer's browser suite (--project hostnames) 22/22. A VM reboot and a Traefik restart came back on their own.applyin 2 min 41 s with 19 checks passed, but the first sign-in ended in a 502:upstream sent too big headeron Platform's sign-in callback, whose session cookie chunks hold the Keycloak ID and refresh tokens. 4k fails, 8k passes; withproxy_buffer_size 16kthe suite passed 22/22. Everyverifycheck passes on the broken install.run-e2e.shfirst stopped athelm dependency buildwithno repository definition for https://libredb.org/libredb-studio/, which a fresh Helm cannot build fromChart.lockwithouthelm repo add; CI copies the embedded chart, so it never ran the guide's command. With the repository added: PASS, browser suite 7/7, revocation in 576 ms.seal-envelopes: ... port 5432 failed: Connection refusedand the release leftpending-upgrade. k3s's kube-router refuses a new pod's first connection for about 100 ms until it holds the pod's address, and the init container made one psql call; thepodFailurePolicycovers only thebootstrapcontainer, so each retry raced again. With the wait of fc8e5d3:run-rotation.sh4/4 on both clusters, andrun-restore.shPASS.helm-install.k8s.tsreported its first test passed, since the callback URL is not/login, with the second test failing on the lost session. With the chart's newproxy-buffer-size: 16kannotation, 7/7; without it, the hardened first test now fails (Received: undefined). PostgreSQL killed: endpoints back in 1 s; Platform, Studio and Keycloak killed at once: 68 s; VM reboot: 77 s; the suite 7/7 afterwards.run-e2e.shPASS (install 64 s, revocation 574 ms) andrun-argocd.shPASS (Synced and Healthy in 81 s).docker container prune, which removed the containers of stopped LibreDB services;docker compose start studiothen started nothing and exited 0. Dokploy's "Clean unused volumes" (docker volume prune --all) during the restore runbook's stop deletedlibredb_platform-uploads, and the nextapplyrecreated it empty with every check passing. The kill switch runbook now usesup -d, which the runbooks drill checks after pruning the stopped Studio container (7/7 on CapRover), and the installer README describes both cleanups.tls.mode: external: aDOCKER-USERrule on port 8080 alone also drops the apps' calls tokeycloak:8080and fails 4verifychecks; the README now gives a rule on the external interface. A wrongtrustedProxiesbreaks Studio's sign-in only, whichstudio-oidc-chaincaught as anhttp://redirect_uri; its cause now namestrustedProxiesand the gateway oflibredb_edge.run-upgrade.shandrun-lifecycle.sh(no next-release chart or lifecycle licenses at hand),run-offline.sh,run-soak.sh,run-openshift.sh,run-external.sh;run-resilience.shrestarts a kind node, so the k3s checks above were done by hand.Test Environment
Screenshots (if applicable)
Not attached; the browser suite saves a screenshot per step under
LIBREDB_E2E_SCREENSHOTS, and the kind and OpenShift runs are available on request.Checklist
bun run coverage:checkininstaller/)Additional Notes
Decisions worth a look during review:
bundle.test.ts, so the chart runs the installer's exact code without a separate jobs image.install.libredb.org/installationand no Helm or Argo CD tracking label, so a prune never deletes the seal keys; uninstall leaves them and the volumes on purpose.bun run build:helm-jobs.Found along the way and not changed here:
x/net, with 2.11.7 already the latest; PostgreSQL's fixable ones are all ingosu, which runs only as root, and these pods are not; Platform 1.0.1 has a CRITICALperl-baseCVE from its Debian base; Platform and Studio both shipnpm, whosebrace-expansionandundiciare flagged and unused at runtime. No image carries a cosign signature.Connection to libredb-postgres:5432 refused) before PostgreSQL is ready, on the first install and after a node reboot, and recovers on its own; an init wait would remove the restart.chart.test.tsfalls back tohelm dependency buildwhen no embedded chart exists, which fails on a fresh machine withouthelm repo add.docker compose start, which a PaaS prune between its steps would turn into a no-op./opt/libredbforpostgres-dataandplatform-uploadswould put them out of reach ofdocker volume prune.Not verified: EKS, GKE and AKS with their own load balancers, and an upgrade to a release with new migrations, which needs the next release.