Skip to main content
This page provides instructions on how to configure the Helm chart to install the Control Plane in any cloud provider. The chart deploys one Control Plane: a Deployment, a Secret holding its environment, a Service for the HTTP API, and — if you ask for them — a ServiceAccount and a pair of Gateway API resources. It depends on no other chart. It needs a PostgreSQL you provide, and nothing else. The Control Plane carries no traffic. It serves the HTTP API and the web app and administers a fleet of Sidecars. There is no gRPC listener on 8010, no protocol proxies, and of the six transport plugins only Slack is started.

Quick Start

A Control Plane with local authentication, in three steps. At the end the web app answers on your machine and you can create the first account.
1

Deploy a PostgreSQL

Skip this if you already have a database to point at. This one is a throwaway with a password in plain sight.
The Control Plane runs its migrations and the organization bootstrap before it listens, so the database has to be reachable at startup.
2

Write the values file

values.yaml
Two keys are required and the rest have defaults. API_URL is the address the web app and every Sidecar reach this on — here it is the port-forward, because that is where you will open it. Change it when you publish the service, and read API_URL before you leave it out.
3

Install the chart

The last bootstrap line names the mode and the address it will serve:
Forward the port and open the web app:
http://127.0.0.1:8009/login — authentication is local by default, so the first visit asks you to create the account. Confirm the mode at the same time:
Delete everything this created with kubectl delete namespace hoop.

Helm Install

To install the latest version in a new namespace (example: hoop):
The release name prefixes every resource the chart owns: A release named hoopcontrolplane collapses to the bare name rather than repeating it.

Overriding values

It is possible to add new attributes or overwrite an attribute from a base values.yaml file. In the example below a specific image tag is pinned and the log level is raised.

Control Plane Configuration

Everything under config becomes an environment variable in a Secret attached with envFrom. A change to any of them rolls the pods on helm upgrade: the Deployment carries a checksum of the rendered Secret.
Those two are required. Everything else below has a default or is optional.

Database

The Control Plane stores its state in PostgreSQL, using the private schema for its own tables. This creates the database and a user with the privileges it needs:
If the password contains special characters, URL-encode it in the connection string. Append ?sslmode=disable if your database does not support TLS.
The chart provisions no database and pglite:// is refused: it is single-node and serves one connection at a time.

Authentication

Authentication is local by default. The Control Plane manages users and passwords itself and signs its own tokens, so a database and an address are the whole configuration. Setting any IDP_* key selects OIDC instead:
IDP_CLIENT_ID and IDP_CLIENT_SECRET become required once IDP_ISSUER is set; the chart refuses to render without them. The provider’s callback is <API_URL>/api/callback — register that URL with your identity provider. See Identity Providers for the per-provider settings.

API URL

API_URL is the address the web app and every Sidecar reach this deployment on. It is required, and it must carry a scheme.
The process itself falls back to http://127.0.0.1:8009 when the variable is absent, which is why the chart requires it: the fallback boots a pod that looks healthy and is not usable.

TLS

Two keys, and one rule: set both to serve HTTPS, leave both empty to serve plaintext.
Nothing is generated, and half a pair serves plaintext rather than failing. Both values go through the environment loader, so three forms work:
The certificate file may carry the root and intermediate CAs as well. Order matters:
Nothing here reaches the probes, because the chart renders none — an httpGet probe you write against a TLS listener must name scheme: HTTPS itself.

Logging

Chart Configuration

Everything below is a chart attribute rather than part of the Control Plane’s environment: how the pod is built, what it is published as, and where it is scheduled.

Image Configuration

By default the chart pulls hoophq/hoopcontrolplane:latest, which is the Ubuntu flavour.
The web UI is compiled into the binary, so neither flavour carries webapp files and both serve the app with no configuration. That is also what makes a distroless flavour possible. Container Images covers what each flavour contains.

Probes

The chart renders no probe. readinessProbe and livenessProbe are both empty by default, and whatever you put under either one is rendered verbatim — any handler the Kubernetes Probe schema accepts, plus timings and thresholds.
Health is GET /api/healthz on 8009, under API_URL’s path if it has one. Ten seconds and not five: the listener opens only after the migrations, the organization bootstrap and the identity provider, so an answer at all means the bootstrap finished. Add scheme: HTTPS inside httpGet when TLS is configured. A tcpSocket check on 8009 serves either listener from one stanza, at the cost of not calling the endpoint:
Why there is no default. The chart cannot know whether the listener speaks TLS. TLS_CERT and TLS_KEY decide it, and either can arrive through extraSecret or existingSecret, which no template can read. The kubelet does not negotiate, so a wrong scheme does not degrade — it stops the pod from ever becoming ready:
That is Go answering Client sent an HTTP request to an HTTPS server. Certificate validity is not the issue in the other direction: kubelet HTTPS probes always skip verification and there is no field to change that.With neither probe set, the kubelet treats the container as ready as soon as it starts and never restarts it for health — a pod still running migrations takes Service traffic. Set them.

Service

One Service, one port.
type: LoadBalancer with the annotations your cloud provider expects is the whole configuration for a cluster without the Gateway API:
There is deliberately no source-range or traffic-policy plumbing. One HTTP port does not need it.

Exposing it with the Gateway API

The chart renders a Gateway and an HTTPRoute rather than an Ingress. This requires the Gateway API CRDs in your cluster — istioctl install for Istio, or the upstream guides for anything else.
Both parentRefs and rules have working defaults. The default rule is a PathPrefix on API_URL’s path, backed by this chart’s Service on 8009 — which is the whole Control Plane, since it serves one HTTP port and nothing else:
To attach to a Gateway somebody else owns, set createGateway: false and name it:
The chart refuses to render createGateway: false with no parentRefs: a route with no parent attaches to nothing and looks installed. It refuses service.enabled: false with no rules for the same reason from the other end — the default route’s backend is that Service, so the route would resolve to nothing and every request would get a 500 from an install that looked fine. listeners, addresses, infrastructure and backendTLS pass through to the Gateway untouched, and setting rules replaces the default rule entirely. There is no Ingress template and no GRPCRoute.
backendTLS is the client certificate the Gateway presents to backends, for mTLS, and it lives in the Gateway API experimental channel. The standard channel’s Gateway CRD has no such field and prunes it silently, so it renders into the manifest and then disappears from the live object — check with kubectl get gateway <name> -o yaml before relying on it.Validating the backend’s own certificate is a different thing: a BackendTLSPolicy resource, which this chart does not render.

Extra Environment Variables

Anything this chart does not render goes in extraSecret, which becomes a second Secret attached with envFrom:
existingSecret references a Secret you manage yourself. The chart never creates or updates it:
Order matters, because later entries win: the chart’s own Secret, then extraSecret, then existingSecret.
A Secret referenced by existingSecret is invisible to the chart, so editing it changes no checksum and helm upgrade rolls nothing. Run kubectl rollout restart deploy/<release>-hoopcontrolplane after you change it, or use a controller that annotates the pods for you.

Extra Volumes

For what the configuration references by path: a TLS certificate under file:///..., an identity provider CA bundle.
Mount any Secret holding a private key with defaultMode: 0400. Kubernetes writes secret files 0644 by default.

Service Account

With create: true the chart makes one named after the release. With create: false, name points at an account that already exists — the shape workload identity wants.
Leaving both unset means the pod uses the namespace default.

Replicas and Deployment Strategy

Read this before raising replicas.Slack’s socket mode opens one websocket per process and nothing coordinates them — no lease, no advisory lock, only in-process mutexes. With Slack configured for an organization, two replicas post every review twice and race each other’s clicks. Slack documents that a payload may go to any open connection with no pattern to rely on, so a click can be handled by the replica that is not holding the waiting session: the verdict is written and the session is never released.Raising replicas is safe when no organization in this deployment has Slack configured. Everything else in the process is stateless HTTP over a shared database.The chart cannot enforce this — whether Slack is configured lives in a database row no chart can read — so it prints a warning above one replica and leaves the decision to you.The same constraint applies across deployments: a Gateway and a Control Plane pointed at the same database both read that row and both open a socket.
The strategy is Recreate for the same reason. At one replica, RollingUpdate’s default surge runs two pods through every upgrade, which is two Slack sockets on a timer. The Control Plane holds no sessions, so the short API gap drops nothing. A deployment already running several replicas has accepted that condition — set RollingUpdate there.

Computing Resources

The chart is API-only: it carries no sessions and runs no proxies.

Node Selector

This configuration describes a pod that has a node selector, disktype: ssd. This means that the pod will get scheduled on a node that has a disktype=ssd label. See this documentation for more information.

Tolerations

See this article explaining how to configure tolerations

Node Affinity

See this article explaining how to configure affinity and anti-affinity rules

Security Context

Both are empty by default and pass through untouched.
The image already runs as uid 10001.

Annotations

Naming

nameOverride changes the base the resource names are built from; fullnameOverride replaces the whole name and drops the release prefix.
Both feed the Deployment’s selector, which is immutable in Kubernetes. Changing either on a live release makes helm upgrade fail. Settle them at install time.

Generating Manifests

If you prefer using manifests over Helm, we recommend this approach. It allows you to track any modifications to the chart whenever a new version appears. You can apply a diff to your versioned files to identify what has been altered.

Next

Connect a Sidecar

Issue a token, point a Sidecar at the Control Plane, and confirm it picked up its configuration.

Environment Variables

Every variable the service reads.