Helm Install Guide
Orchestra ships as a single Helm chart that includes the API server, operator, frontend, and an optional bundled oauth2-proxy.
Prerequisites
Section titled “Prerequisites”| Requirement | Notes |
|---|---|
| Kubernetes 1.25+ | CEL validation on CRDs requires 1.25+ |
| Helm 3.12+ | |
| Ingress controller | Traefik or nginx-ingress — see Ingress Guide |
| cert-manager | For automatic TLS; can be skipped in dev |
| PostgreSQL 14+ | External — see Database below |
| Google OAuth credentials | See oauth2-proxy Setup |
Quick install
Section titled “Quick install”- Create the OAuth secret:
kubectl create secret generic orchestra-oauth-secrets \ --namespace orchestra-system \ --from-literal=client-id=<google-client-id> \ --from-literal=client-secret=<google-client-secret> \ --from-literal="cookie-secret=$(python3 -c 'import secrets; print(secrets.token_hex(16))')"- Install the chart:
helm install orchestra deploy/charts/orchestra \ --namespace orchestra-system \ --create-namespace \ --set global.domain=orchestra.example.edu \ --set global.defaultNamespace=default \ --set ingress.controller=traefik \ --set oauth2Proxy.fullProxy=true \ --set api.database.existingSecret=orchestra-db \ --set "api.adminEmails={admin@example.edu}" \ -f oauth2-proxy-values.yamlThe oauth2-proxy credentials and domain restrictions live in the bundled
subchart and cannot be set cleanly via --set (the subchart key contains a
dot: "oauth2-proxy"). Put them in a small values file:
"oauth2-proxy": config: existingSecret: orchestra-oauth-secrets configFile: |- email_domains = [ "example.edu" ] upstreams = [ "http://orchestra-frontend.orchestra-system.svc.cluster.local:80" ] extraArgs: redirect-url: "https://app.orchestra.example.edu/oauth2/callback" cookie-domain: ".orchestra.example.edu" whitelist-domain: ".orchestra.example.edu" set-xauthrequest: "true" skip-provider-button: "true"See The bundled oauth2-proxy subchart
below for the full structure, and oauth2-proxy Setup for
provider details. For production, use a values file for everything instead of
--set flags — see Production deployment.
Database
Section titled “Database”Orchestra requires an external PostgreSQL database (version 14+; the GCP reference deployment uses PostgreSQL 18). The operator and API share a single database.
Create the secret
Section titled “Create the secret”kubectl create secret generic orchestra-db \ --namespace orchestra-system \ --from-literal=database-url='postgresql+asyncpg://orchestra:password@host:5432/orchestra'Then reference it in your values:
api: database: existingSecret: orchestra-db secretKey: database-url # default; change if your secret uses a different keyDirect URL (dev only)
Section titled “Direct URL (dev only)”api: database: url: "postgresql+asyncpg://orchestra:orchestra@localhost:5433/orchestra"Cloud SQL on GCP
Section titled “Cloud SQL on GCP”The recommended pattern on GKE is the Cloud SQL Auth Proxy sidecar with Workload Identity (no password needed). See GCP Autopilot Guide for a complete walkthrough.
Ingress and auth
Section titled “Ingress and auth”Auth is applied at the ingress layer. Set ingress.controller to match what’s
installed in your cluster:
ingress: controller: traefik # "traefik" | "nginx" | "custom" className: traefik tls: enabled: true clusterIssuer: letsencrypt-prodWith ingress.controller=traefik and oauth2Proxy.enabled=true the chart
creates four Middleware objects — orchestra-auth, orchestra-auth-signin,
orchestra-api-cors, and orchestra-auth-headers — and wires them onto the
frontend and API ingresses. The forwardAuth-redirect annotations are only added
when fullProxy=false; see the Ingress Guide
for the full breakdown.
See the Ingress Guide for per-controller prerequisites and the full auth flow explanation.
Production deployment
Section titled “Production deployment”-
Copy
values-prod.yamlfrom the chart directory and fill in everyTODO:Terminal window cp deploy/charts/orchestra/values-prod.yaml my-values.yaml -
Create the database secret (see above).
-
Install:
Terminal window helm install orchestra deploy/charts/orchestra \--namespace orchestra-system \--create-namespace \-f my-values.yaml
Upgrading
Section titled “Upgrading”# Upgrade the chart (runs the Alembic migration Job automatically as a pre-upgrade hook)helm upgrade orchestra deploy/charts/orchestra \ --namespace orchestra-system \ -f my-values.yamlThe chart runs alembic upgrade head as a pre-install,pre-upgrade Job before
the new API pods start. Check Job logs if the upgrade stalls:
kubectl logs -n orchestra-system job/orchestra-migrate-<revision>Local dev (kind)
Section titled “Local dev (kind)”kind create clusterhelm install orchestra deploy/charts/orchestra \ --namespace orchestra-system \ --create-namespace \ --set api.requireAuthentication=false \ --set api.devIdentity=dev@orchestra.localhost \ --set ingress.tls.enabled=false \ --set oauth2Proxy.enabled=false \ --set "api.adminEmails={dev@orchestra.localhost}"All API calls are attributed to dev@orchestra.localhost, which is also an
admin. No Google credentials required.
Complete values reference
Section titled “Complete values reference”global
Section titled “global”| Key | Default | Description |
|---|---|---|
global.domain | orchestra.localhost | Base domain; produces app.<domain> (frontend) and api.<domain> (API) |
global.defaultNamespace | default | Default Kubernetes namespace for workshop sessions |
global.imagePullSecrets | [] | Names of existing imagePullSecrets in the release namespace |
| Key | Default | Description |
|---|---|---|
api.image.repository | seandavi/orchestra-api | |
api.image.tag | latest | Pin to a release tag in production |
api.replicas | 1 | |
api.adminEmails | [] | Emails with admin access (manage all sessions) |
api.requireAuthentication | true | Set false with devIdentity for local dev |
api.devIdentity | null | Identity bypass email; never set in production |
api.database.existingSecret | "" | Name of Secret containing database-url key |
api.database.secretKey | database-url | Key name within the Secret |
api.database.url | "" | Direct URL fallback; use Secret in production |
api.podDisruptionBudget.enabled | false | Enable PDB (set true with replicas ≥ 2) |
api.podDisruptionBudget.minAvailable | 1 | |
api.podSecurityContext | {runAsNonRoot: true, runAsUser: 1000} | Override for OpenShift (set runAsUser: null) |
api.containerSecurityContext | {allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: {drop: [ALL]}} | |
api.extraEnv | [] | Extra env vars injected into the API container |
api.extraContainers | [] | Extra sidecar containers (e.g. Cloud SQL Auth Proxy) |
api.extraVolumes | [] | Extra volumes |
api.extraVolumeMounts | [] | Extra volume mounts on the API container |
operator
Section titled “operator”| Key | Default | Description |
|---|---|---|
operator.image.tag | latest | |
operator.defaultWorkshopImage | rocker/rstudio:latest | Default container image for new workshop sessions |
operator.logLevel | INFO |
frontend
Section titled “frontend”| Key | Default | Description |
|---|---|---|
frontend.image.tag | latest | |
frontend.replicas | 1 |
persistence
Section titled “persistence”| Key | Default | Description |
|---|---|---|
persistence.storageClass | "" | Storage class for session PVCs; "" = cluster default |
persistence.size | 10Gi | PVC size per workshop session |
ingress
Section titled “ingress”| Key | Default | Description |
|---|---|---|
ingress.enabled | true | |
ingress.controller | traefik | traefik | nginx | custom — see Ingress Guide |
ingress.className | traefik | Kubernetes ingressClassName; "" = cluster default |
ingress.tls.enabled | true | |
ingress.tls.clusterIssuer | letsencrypt-prod | cert-manager ClusterIssuer; "" to manage TLS yourself |
ingress.annotations | {} | Extra annotations merged onto every Ingress; sole annotations when controller=custom |
networkPolicy
Section titled “networkPolicy”| Key | Default | Description |
|---|---|---|
networkPolicy.enabled | false | Requires a CNI with NetworkPolicy support (Calico, Cilium, GKE Dataplane V2) |
oauth2Proxy
Section titled “oauth2Proxy”The Orchestra chart itself reads only two oauth2-proxy keys. They control
whether the bundled proxy is deployed and which auth mode the ingress uses.
Credentials and domain rules are not set here — they go in the separate
top-level "oauth2-proxy" subchart block (see below).
| Key | Default | Description |
|---|---|---|
oauth2Proxy.enabled | true | Deploy the bundled oauth2-proxy and create the Traefik auth middlewares |
oauth2Proxy.fullProxy | false | Auth mode toggle — see below. true is recommended for Traefik |
fullProxy — the auth-mode toggle
Section titled “fullProxy — the auth-mode toggle”| Mode | Behaviour |
|---|---|
fullProxy: false (default) | ForwardAuth mode. The frontend ingress routes directly to the frontend; the Traefik middleware calls oauth2-proxy’s /oauth2/auth to validate each request. Note: Traefik v3’s errors middleware does not emit a clean 302 on auth failure, so unauthenticated browser requests get a 401 with a Location header rather than a transparent redirect. |
fullProxy: true | Full proxy mode. oauth2-proxy sits in front of the frontend: the frontend ingress routes to oauth2-proxy, which proxies authenticated traffic to the frontend and redirects unauthenticated users to Google. The API ingress still uses ForwardAuth for identity injection. Recommended for Traefik because the login redirect is seamless. Both values-prod.yaml and the GCP reference deployment set this to true. |
The bundled oauth2-proxy subchart
Section titled “The bundled oauth2-proxy subchart”OAuth credentials, allowed domains, redirect URLs, and cookie settings are
passed straight through to the bundled
oauth2-proxy
subchart. These keys must live at the root of your values file under the
quoted "oauth2-proxy" key — not nested under oauth2Proxy. Because the key
contains a dot, prefer a values file over --set.
# Root-level — passed verbatim to the oauth2-proxy subchart"oauth2-proxy": config: # Reference a Secret with client-id / client-secret / cookie-secret keys existingSecret: orchestra-oauth-secrets # Or inline (dev only): clientID / clientSecret / cookieSecret configFile: |- email_domains = [ "example.edu" ] # use "*" to allow any Google account upstreams = [ "http://orchestra-frontend.orchestra-system.svc.cluster.local:80" ] extraArgs: redirect-url: "https://app.orchestra.example.edu/oauth2/callback" cookie-domain: ".orchestra.example.edu" whitelist-domain: ".orchestra.example.edu" set-xauthrequest: "true" # inject X-Auth-Request-* headers upstream skip-provider-button: "true" # go straight to Google, skip the chooser page # Optional per-email allowlist (in addition to / instead of email_domains) authenticatedEmailsFile: enabled: true restricted_access: |- alice@example.edu bob@other.eduSee oauth2-proxy Setup for credential creation, provider configuration (Google / GitHub), and cookie-secret rotation.
Disabling the bundled oauth2-proxy
Section titled “Disabling the bundled oauth2-proxy”If your cluster already has an auth proxy (Cloudflare Access, corporate oauth2-proxy fleet, GKE IAP, etc.):
oauth2Proxy: enabled: falseingress: controller: custom annotations: # Add whatever annotations your proxy requires example.com/auth: "true"Your proxy must forward X-Auth-Request-Email: <user@domain> to the API.
Orchestra uses this header to identify the caller and enforce ownership; it does
not have its own login UI.