Skip to content

Helm Install Guide

Orchestra ships as a single Helm chart that includes the API server, operator, frontend, and an optional bundled oauth2-proxy.

RequirementNotes
Kubernetes 1.25+CEL validation on CRDs requires 1.25+
Helm 3.12+
Ingress controllerTraefik or nginx-ingress — see Ingress Guide
cert-managerFor automatic TLS; can be skipped in dev
PostgreSQL 14+External — see Database below
Google OAuth credentialsSee oauth2-proxy Setup
  1. Create the OAuth secret:
Terminal window
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))')"
  1. Install the chart:
Terminal window
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.yaml

The 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-values.yaml
"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.

Orchestra requires an external PostgreSQL database (version 14+; the GCP reference deployment uses PostgreSQL 18). The operator and API share a single database.

Terminal window
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 key
api:
database:
url: "postgresql+asyncpg://orchestra:orchestra@localhost:5433/orchestra"

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.

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-prod

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

  1. Copy values-prod.yaml from the chart directory and fill in every TODO:

    Terminal window
    cp deploy/charts/orchestra/values-prod.yaml my-values.yaml
  2. Create the database secret (see above).

  3. Install:

    Terminal window
    helm install orchestra deploy/charts/orchestra \
    --namespace orchestra-system \
    --create-namespace \
    -f my-values.yaml
Terminal window
# 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.yaml

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

Terminal window
kubectl logs -n orchestra-system job/orchestra-migrate-<revision>
Terminal window
kind create cluster
helm 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.

KeyDefaultDescription
global.domainorchestra.localhostBase domain; produces app.<domain> (frontend) and api.<domain> (API)
global.defaultNamespacedefaultDefault Kubernetes namespace for workshop sessions
global.imagePullSecrets[]Names of existing imagePullSecrets in the release namespace
KeyDefaultDescription
api.image.repositoryseandavi/orchestra-api
api.image.taglatestPin to a release tag in production
api.replicas1
api.adminEmails[]Emails with admin access (manage all sessions)
api.requireAuthenticationtrueSet false with devIdentity for local dev
api.devIdentitynullIdentity bypass email; never set in production
api.database.existingSecret""Name of Secret containing database-url key
api.database.secretKeydatabase-urlKey name within the Secret
api.database.url""Direct URL fallback; use Secret in production
api.podDisruptionBudget.enabledfalseEnable PDB (set true with replicas ≥ 2)
api.podDisruptionBudget.minAvailable1
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
KeyDefaultDescription
operator.image.taglatest
operator.defaultWorkshopImagerocker/rstudio:latestDefault container image for new workshop sessions
operator.logLevelINFO
KeyDefaultDescription
frontend.image.taglatest
frontend.replicas1
KeyDefaultDescription
persistence.storageClass""Storage class for session PVCs; "" = cluster default
persistence.size10GiPVC size per workshop session
KeyDefaultDescription
ingress.enabledtrue
ingress.controllertraefiktraefik | nginx | custom — see Ingress Guide
ingress.classNametraefikKubernetes ingressClassName; "" = cluster default
ingress.tls.enabledtrue
ingress.tls.clusterIssuerletsencrypt-prodcert-manager ClusterIssuer; "" to manage TLS yourself
ingress.annotations{}Extra annotations merged onto every Ingress; sole annotations when controller=custom
KeyDefaultDescription
networkPolicy.enabledfalseRequires a CNI with NetworkPolicy support (Calico, Cilium, GKE Dataplane V2)

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

KeyDefaultDescription
oauth2Proxy.enabledtrueDeploy the bundled oauth2-proxy and create the Traefik auth middlewares
oauth2Proxy.fullProxyfalseAuth mode toggle — see below. true is recommended for Traefik
ModeBehaviour
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: trueFull 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.

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

See oauth2-proxy Setup for credential creation, provider configuration (Google / GitHub), and cookie-secret rotation.

If your cluster already has an auth proxy (Cloudflare Access, corporate oauth2-proxy fleet, GKE IAP, etc.):

oauth2Proxy:
enabled: false
ingress:
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.