Skip to main content

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

Terminal window
helm repo add plumber-platform https://getplumber.github.io/plumber-platform
helm repo update
helm upgrade --install plumber plumber-platform/plumber-platform \
-n plumber --create-namespace -f values.yaml

The 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.

  1. ๐Ÿ“„ Configure the domain name

    Info

    Plumber needs a domain of its own: with mydomain.com, the Plumber URL is typically https://plumber.mydomain.com.

    Create the DNS record

    1. Name: <your_plumber_domain>
    2. Type: A
    3. Content: <your-cluster-public-ip>

    Caution

    One host serves both apps (why): the chartโ€™s Ingress routes /api to the backend and / to the frontend on the single ingress.host.

  2. ๐ŸฆŠ 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 ID and Secret somewhere safe: they are used by the first-run bootstrap below, and they end up encrypted in the Platform database, never in your values file.

  3. ๐Ÿ” Create the secrets

    The recommended path stores every credential in Kubernetes Secrets you own, and points the chart at them with the existingSecret keys.

    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 a redis:// 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.password and redis.auth.password inline in your values instead. The chart then writes them into the Secret <release>-plumber-platform.

  4. โš™๏ธ 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 audience
    gitlabUrl: https://gitlab.mydomain.com
    existingSecret: plumber-secrets # key tokenEncryptionKey
    ingress:
    enabled: true
    className: nginx
    host: plumber.mydomain.com # ONE host: /api -> backend, / -> frontend
    tls:
    enabled: true # secret <release>-plumber-platform-tls unless tls.secretName
    postgresql:
    deploy: true
    global:
    postgresql:
    auth:
    existingSecret: plumber-pg # key password
    redis:
    deploy: true
    auth:
    existingSecret: plumber-redis # key password

    platform.baseUrl is the public origin of the install, without a trailing slash. It is also the aud claim your pipelinesโ€™ OIDC tokens must carry, unless you override platform.oidc.audience. platform.oidc.allowedIssuers defaults to [platform.gitlabUrl].

    Ingress

    ingress:
    enabled: true
    className: 'nginx' # the class of your ingress controller
    host: 'plumber.mydomain.com'
    annotations: # whatever your controller or certificate manager needs
    cert-manager.io/cluster-issuer: 'letsencrypt-production'
    tls:
    enabled: true
    secretName: '' # defaults to <release>-plumber-platform-tls

    PostgreSQL

    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 PostgreSQL StatefulSet, its Service and a PersistentVolumeClaim for 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: true
    custom:
    dbName: 'plumber'
    sslmode: 'disable'
    port: 5432
    global:
    postgresql:
    auth:
    username: plumber
    existingSecret: 'plumber-pg'
    secretKeys:
    userPasswordKey: 'password'
    # Persistence for the PostgreSQL data directory
    persistence:
    size: '10Gi'
    storageClass: '' # leave empty to use the cluster default StorageClass
    accessMode: 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: IfNotPresent

    Option B: Use an external PostgreSQL

    postgresql:
    deploy: false
    custom:
    host: REPLACE_ME_BY_POSTGRES_HOST
    dbName: 'plumber'
    sslmode: 'require'
    port: 5432
    global:
    postgresql:
    auth:
    username: plumber
    existingSecret: 'plumber-pg'
    secretKeys:
    userPasswordKey: 'password'

    Redis

    Same two options.

    # Option A: deployed by the chart
    redis:
    deploy: true
    auth:
    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:

    1. Reference an existing Secret (existingSecret) or ConfigMap (configMapName) holding the certificates.
    2. Or add the CA public root certificate inline with the certificates key.
    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 imageRegistry and imagePullSecrets to pull the images from your own registry.

  5. ๐Ÿš€ Install the chart

    Terminal window
    helm upgrade --install plumber plumber-platform/plumber-platform \
    -n plumber --create-namespace -f custom_values.yaml

    Tip

    You have successfully installed Plumber on your Kubernetes cluster ๐ŸŽ‰

  6. ๐Ÿ”‘ 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 install prints 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 instance

    To 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-bootstrap refuses 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 Secret instead.

    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: true
    gitlabUrl: https://gitlab.mydomain.com
    clientId: <application-id>
    scope: instance # or group, with rootGroup
    rootGroup: ''
    existingSecret: plumber-bootstrap # key clientSecret, plus token for a group

    The 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 15m

    Caution

    Pass --timeout 15m whenever bootstrap.enabled is 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.

  7. ๐Ÿ‘ค First login

    Open https://<your_plumber_domain> and sign in with GitLab as a Plumber Admin to finish the setup.

  8. ๐Ÿงฉ 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-platform
    cd plumber-platform
    read -rs -p "GitLab token: " PLUMBER_COMPONENT_TOKEN; echo; export PLUMBER_COMPONENT_TOKEN
    ./scripts/component-mirror.sh --gitlab-url https://gitlab.mydomain.com --group my-group

    What 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:

  • nginx as ingress controller
  • cert-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 chart
postgresql:
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 Deployment uses the Recreate strategy 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). A startupProbe gives the first boot the time to migrate the database.
  • backend.terminationGracePeriodSeconds defaults to 1200: in-flight background work can take many minutes to drain on a large estate.
  • Prometheus metrics are served on /metrics on the backend Service, and are not exposed by the ingress.

โซ Update

  1. Update the Plumber Helm repository

    Terminal window
    helm repo update
  2. Bump front.tag and backend.tag together in your values: every release pins both images to one version.

    front:
    tag: v2.0.0
    backend:
    tag: v2.0.0
  3. Run the helm upgrade

    Terminal window
    helm upgrade --install plumber plumber-platform/plumber-platform \
    -n plumber -f custom_values.yaml
  4. You 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.