Kubernetes Installation
This page describes how to run a self-managed instance of the Plumber Platform on Kubernetes, with the plumber-platform Helm chart.
๐ป Requirements
On top of the common requirements (GitLab version, network access, DNS), this method needs:
- A PostgreSQL instance (or let the chart deploy PostgreSQL 18 for you)
- A Redis instance (or let the chart deploy Redis 8.4 for you)
- A Kubernetes cluster with:
- One ingress controller (e.g. Nginx or Traefik)
- A certificate manager with an ACME provider: cert-manager
- If you let the chart deploy PostgreSQL: the ability to provision persistent volumes
- Helm 3 and Kubectl on your machine, to talk to the Kubernetes API
๐ ๏ธ Installation
helm repo add plumber-platform https://getplumber.github.io/plumber-platformhelm repo updatehelm upgrade --install plumber plumber-platform/plumber-platform \ -n plumber --create-namespace -f values.yamlThe chart is named plumber-platform, and its version matches the platform version it installs (chart 2.0.0 installs v2.0.0). Both application images are pinned by the chart and always move together.
๐ Configure the domain name
Info
Plumber needs a domain of its own: with
mydomain.com, the Plumber URL is typicallyhttps://plumber.mydomain.com.Create the DNS record
- Name:
<your_plumber_domain> - Type:
A - Content:
<your-cluster-public-ip>
Caution
One host serves both apps (why): the chartโs Ingress routes
/apito the backend and/to the frontend on the singleingress.host.- Name:
๐ฆ Create the GitLab OAuth application
Create the GitLab OAuth application with the redirect URI
https://<your_plumber_domain>/api/v1/auth/callback.Store the
Application IDandSecretsomewhere safe: they are used by the first-run bootstrap below, and they end up encrypted in the Platform database, never in your values file.๐ Create the secrets
The recommended path stores every credential in Kubernetes Secrets you own, and points the chart at them with the
existingSecretkeys.Plumber secret: the key that seals every GitLab secret the Platform stores. It must be exactly 64 hexadecimal characters, and it cannot be rotated in place: back it up.
Terminal window kubectl create secret generic plumber-secrets -n plumber \--from-literal=tokenEncryptionKey="$(openssl rand -hex 32)"PostgreSQL secret (key
password, used whether the chart deploys PostgreSQL or you bring your own):Terminal window kubectl create secret generic plumber-pg -n plumber \--from-literal=password="<postgres-password>"Redis secret (key
password; URL-safe characters only, the value is embedded in aredis://URL):Terminal window kubectl create secret generic plumber-redis -n plumber \--from-literal=password="<redis-password>"Info
For an evaluation setup you can skip the Secrets and put
platform.tokenEncryptionKey,postgresql.global.postgresql.auth.passwordandredis.auth.passwordinline in your values instead. The chart then writes them into the Secret<release>-plumber-platform.โ๏ธ Configure your values
Every key is commented in the chartโs default
values.yaml, and a full example closes this page.Info
The sections below assume your own values file sits in the current directory, named
custom_values.yaml.Minimal values
platform:baseUrl: https://plumber.mydomain.com # public origin, also the OIDC audiencegitlabUrl: https://gitlab.mydomain.comexistingSecret: plumber-secrets # key tokenEncryptionKeyingress:enabled: trueclassName: nginxhost: plumber.mydomain.com # ONE host: /api -> backend, / -> frontendtls:enabled: true # secret <release>-plumber-platform-tls unless tls.secretNamepostgresql:deploy: trueglobal:postgresql:auth:existingSecret: plumber-pg # key passwordredis:deploy: trueauth:existingSecret: plumber-redis # key passwordplatform.baseUrlis the public origin of the install, without a trailing slash. It is also theaudclaim your pipelinesโ OIDC tokens must carry, unless you overrideplatform.oidc.audience.platform.oidc.allowedIssuersdefaults to[platform.gitlabUrl].Ingress
ingress:enabled: trueclassName: 'nginx' # the class of your ingress controllerhost: 'plumber.mydomain.com'annotations: # whatever your controller or certificate manager needscert-manager.io/cluster-issuer: 'letsencrypt-production'tls:enabled: truesecretName: '' # defaults to <release>-plumber-platform-tlsPostgreSQL
Let the chart deploy a PostgreSQL instance, or point it at an external one.
Option A: Deploy PostgreSQL via the chart
When
postgresql.deploy: true, the chart provisions a single-replica PostgreSQLStatefulSet, itsServiceand aPersistentVolumeClaimfor the data directory.Caution
The bundled PostgreSQL is meant for evaluation, development and small self-managed deployments. For production, use a managed database through Option B.
postgresql:deploy: truecustom:dbName: 'plumber'sslmode: 'disable'port: 5432global:postgresql:auth:username: plumberexistingSecret: 'plumber-pg'secretKeys:userPasswordKey: 'password'# Persistence for the PostgreSQL data directorypersistence:size: '10Gi'storageClass: '' # leave empty to use the cluster default StorageClassaccessMode: ReadWriteOnce# To pull from a private registry or pin an exact version:#image:# registry: my-private-registry.example.com # omit to use Docker Hub# repository: postgres# tag: "18"# digest: "" # optional: pin exact version with sha256:...# pullPolicy: IfNotPresentOption B: Use an external PostgreSQL
postgresql:deploy: falsecustom:host: REPLACE_ME_BY_POSTGRES_HOSTdbName: 'plumber'sslmode: 'require'port: 5432global:postgresql:auth:username: plumberexistingSecret: 'plumber-pg'secretKeys:userPasswordKey: 'password'Redis
Same two options.
# Option A: deployed by the chartredis:deploy: trueauth:existingSecret: 'plumber-redis'existingSecretPasswordKey: 'password'# Option B: your own instance#redis:# deploy: false# custom:# host: REPLACE_ME_BY_REDIS_HOST# port: 6379# databaseIndex: 0# auth:# existingSecret: 'plumber-redis'# existingSecretPasswordKey: 'password'(Optional) Add your custom Certificate Authority
If your GitLab instance (or your SMTP server) presents a privately-signed certificate, give the chart your root CA. Two ways:
- Reference an existing
Secret(existingSecret) orConfigMap(configMapName) holding the certificates. - Or add the CA public root certificate inline with the
certificateskey.
customCertificateAuthority:existingSecret: ""configMapName: ""certificates: []# - name: rootCA.crt # Must have the .crt or .pem extension# value: |# -----BEGIN CERTIFICATE-----# (SNIPPED FOR BREVITY)# -----END CERTIFICATE-----The chart mounts them in both apps, and an init container concatenates them into the single PEM bundle the backend reads.
(Optional) Air-gapped installs
The backend polls a manifest once a day to tell you a new release exists. Disable it:
platform:updateCheckUrl: "off"Set
imageRegistryandimagePullSecretsto pull the images from your own registry.- Reference an existing
๐ Install the chart
Terminal window helm upgrade --install plumber plumber-platform/plumber-platform \-n plumber --create-namespace -f custom_values.yamlTip
You have successfully installed Plumber on your Kubernetes cluster ๐
๐ First run: configure the GitLab connection
The GitLab connection is configured once by
plumber-bootstrap. Choose one of the two ways.Manual command (the one
helm installprints in its NOTES):Terminal window kubectl exec -n plumber deploy/plumber-backend -- /usr/local/bin/plumber-bootstrap \-base-url https://gitlab.mydomain.com -client-id <application-id> \-client-secret <secret> -scope instanceTo connect a single root group, add
-scope group -root-group <path>and-token <gitlab-access-token>, a token that is Maintainer or above on that group:plumber-bootstraprefuses a group without one.Caution
With the manual command the secret, and the token for a group, are visible in the podโs process list for the duration of the command. The hook Job below reads them from a
Secretinstead.Post-install hook Job: create a Secret with the OAuth application secret (key
clientSecret), then enable the Job:Terminal window kubectl create secret generic plumber-bootstrap -n plumber \--from-literal=clientSecret="<application-secret>"To connect a single root group, the Secret also needs a GitLab access token that is Maintainer or above on that group (key
token), and it becomes the access token Plumber syncs with:Terminal window kubectl create secret generic plumber-bootstrap -n plumber \--from-literal=clientSecret="<application-secret>" \--from-literal=token="<gitlab-access-token>"bootstrap:enabled: truegitlabUrl: https://gitlab.mydomain.comclientId: <application-id>scope: instance # or group, with rootGrouprootGroup: ''existingSecret: plumber-bootstrap # key clientSecret, plus token for a groupThe Job waits for the backend
/readyz, and the backendโs first boot runs the database migrations, which can take a few minutes with the image pull.Terminal window helm upgrade --install plumber plumber-platform/plumber-platform \-n plumber --create-namespace -f custom_values.yaml --timeout 15mCaution
Pass
--timeout 15mwheneverbootstrap.enabledis true, so Helm does not mark the release failed while the Job is still waiting for the backend.Leaving the Job enabled is safe: like the command, it refuses an instance that is already configured. Check it with
kubectl logs -n plumber job/plumber-plumber-platform-bootstrap.๐ค First login
Open
https://<your_plumber_domain>and sign in with GitLab as a Plumber Admin to finish the setup.๐งฉ Plumber component in your GitLab (optional)
If your GitLab cannot reach gitlab.com, copy the Plumber CI/CD component into a project of your instance and publish it to your CI/CD catalog with the script from the installation repository, from any machine that reaches your GitLab:
Terminal window git clone https://github.com/getplumber/plumber-platform.git plumber-platformcd plumber-platformread -rs -p "GitLab token: " PLUMBER_COMPONENT_TOKEN; echo; export PLUMBER_COMPONENT_TOKEN./scripts/component-mirror.sh --gitlab-url https://gitlab.mydomain.com --group my-groupWhat it needs and what to set in Plumber afterwards is described in the Docker Compose guide. Run it again after a Plumber release to publish the new version.
๐ Configuration example
Info
This example runs in a Kubernetes cluster using:
nginxas ingress controllercert-manager- A clusterIssuer named
letsencrypt-production
platform: baseUrl: "https://plumber.mydomain.com" gitlabUrl: "https://gitlab.mydomain.com" existingSecret: "plumber-secrets" # updateCheckUrl: "off" # air-gapped installs config: logLevel: "info" logFormat: "json" sessionTTL: "168h" retentionDays: 30
ingress: enabled: true className: "nginx" host: "plumber.mydomain.com" annotations: cert-manager.io/cluster-issuer: "letsencrypt-production" tls: enabled: true
# Option A: Deploy PostgreSQL via the chartpostgresql: deploy: true custom: dbName: "plumber" sslmode: "disable" port: 5432 global: postgresql: auth: username: plumber existingSecret: "plumber-pg" secretKeys: userPasswordKey: "password" persistence: size: "10Gi"
# Option B: Use an external PostgreSQL#postgresql:# deploy: false# custom:# host: "database-1.REDACTED.us-east-1.rds.amazonaws.com"# port: 5432# dbName: "plumber"# sslmode: "require"# global:# postgresql:# auth:# username: plumber# existingSecret: "plumber-pg"# secretKeys:# userPasswordKey: "password"
redis: deploy: true auth: existingSecret: "plumber-redis" existingSecretPasswordKey: "password"
bootstrap: enabled: true gitlabUrl: "https://gitlab.mydomain.com" clientId: "REDACTED" scope: instance existingSecret: "plumber-bootstrap"๐ฉบ How the backend is deployed
- The backend
Deploymentuses theRecreatestrategy with one replica, by design: the single process owns the job queues, and two versions must never run at once.
Caution
Keep backend.replicaCount at 1. Only one backend replica is supported, whatever the size of your estate: the backend owns its job queues, migrates the database on boot and refreshes each userโs GitLab token in-process, so a second replica would compete for jobs and race those steps. Scale the single backend vertically (backend.resources, PLUMBER_DB_MAX_OPEN_CONNS) instead. front.replicaCount may be raised freely: the frontend is stateless.
- Probes: liveness
/healthz, readiness/readyz(database-backed). AstartupProbegives the first boot the time to migrate the database. backend.terminationGracePeriodSecondsdefaults to1200: in-flight background work can take many minutes to drain on a large estate.- Prometheus metrics are served on
/metricson the backendService, and are not exposed by the ingress.
โซ Update
Update the Plumber Helm repository
Terminal window helm repo updateBump
front.tagandbackend.tagtogether in your values: every release pins both images to one version.front:tag: v2.0.0backend:tag: v2.0.0Run the helm upgrade
Terminal window helm upgrade --install plumber plumber-platform/plumber-platform \-n plumber -f custom_values.yamlYou have successfully updated Plumber ๐
Caution
Upgrades are sequential and additive: the backend migrates the database on boot. To roll back, roll the images back to the previous version. Never run a database downgrade.