Skip to content

Orbital — Installation Guide

Orbital is a self-hosted platform for deploying Streamlit apps from git repositories onto a Kubernetes cluster (SPEC.md). This guide covers a production-style installation with the Helm chart in deploy/chart/orbital. For a local development setup on minikube, see DEVELOPMENT.md instead.

1. Prerequisites

Requirement Notes
Kubernetes ≥ 1.27 any distribution (EKS/GKE/AKS/k3s/minikube/…)
Ingress controller NGINX Ingress recommended; must support websockets
DNS subdomain routing: wildcard *.<apps-domain> → ingress; path routing (apps.routing=path): a single apps host suffices
Container registry reachable by build pods (push) and by cluster nodes (pull)
NetworkPolicy-enforcing CNI required for the chart's default-deny NetworkPolicies (networkPolicy.enabled, default true) to actually isolate the streamlit-apps/streamlit-builds/streamlit-scans namespaces — e.g. Calico, Cilium, AKS with "Azure/Calico" networking, GKE with "Dataplane V2" (Cilium). Not every default CNI enforces NetworkPolicy (e.g. plain kubenet/AWS VPC CNI without add-ons, or minikube's default driver, silently ignore them). If yours doesn't, either enable enforcement or set networkPolicy.enabled=false — shipping policies your CNI won't enforce is a false sense of isolation.
OIDC identity provider Keycloak, Entra ID, Okta, Dex… — required for console RBAC and private apps
Helm ≥ 3.12, Docker on the machine performing the install
TLS on the console host required if auth.console.enabled=true and auth.console.sessionCookieSecure is left at its default (true): the console session cookie is marked Secure and browsers won't return it over plain HTTP. cert-manager (or any ingress-terminated TLS) covers this; a wildcard certificate for the apps domain is optional beyond that.
(optional) metrics-server powers the console's per-app CPU/memory Metrics tab (minikube: addons enable metrics-server) — without it the tab shows "no metrics"

Registry note. The platform builds one container image per app. Configure:

  • registry.pushUrl — endpoint build pods push to (in-cluster DNS is fine).
  • registry.pullPrefix — the image prefix nodes use to pull.

These may differ (e.g. minikube's registry addon: push to registry.kube-system.svc.cluster.local:80, pull via localhost:5000). For a cloud registry both are usually the same host. The chart can also deploy a dev-only in-cluster registry (registry.internal.enabled=true). That in-cluster registry (and build push/pull traffic to it) is plain HTTP by default; set registry.tls.enabled=true for a TLS-protected option — see ADMIN.md#registry-tls for the tradeoffs and setup.

Rootless builds. By default builds run rootless BuildKit. Clusters without user-namespace support (some LXC/nested environments) need builds.rootless=false (privileged build pods).

NetworkPolicy: configure your ingress controller's namespace selector. The chart ships default-deny NetworkPolicy resources for the streamlit-apps/streamlit-builds/streamlit-scans namespaces and the control-plane namespace (networkPolicy.enabled=true by default — see ADMIN.md for what each denies). They only allow ingress from your ingress controller's namespace, matched by networkPolicy.ingressNamespaceSelector — a raw namespaceSelector passed through as-is, because ingress controller label conventions vary by distribution and the chart cannot guess yours. Leaving this unset denies ALL ingress, including to the console and every app (fail closed, not fail open). Find your ingress controller's namespace labels (kubectl get ns <ingress-namespace> -o yaml) and set them, e.g. for a stock ingress-nginx install (which conventionally labels its namespace app.kubernetes.io/name: ingress-nginx):

networkPolicy:
  ingressNamespaceSelector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx

Check your own controller's actual namespace labels rather than assuming this — distributions (EKS/GKE/AKS managed add-ons, k3s' Traefik, etc.) label things differently.

2. Get the platform images and Helm chart

Tagged releases (vX.Y.Z) are built and published automatically by .github/workflows/release.yml:

  • Images: ghcr.io/gggard/orbital/control-plane:X.Y.Z and ghcr.io/gggard/orbital/console:X.Y.Z
  • Helm chart: oci://ghcr.io/gggard/orbital/charts/orbital, version X.Y.Z
helm pull oci://ghcr.io/gggard/orbital/charts/orbital --version 0.1.0 --untar

This is the recommended path if ghcr.io/gggard/orbital is reachable from your cluster's nodes and you don't need a custom registry.

Building your own images instead (custom registry, forked platform code, or a registry your cluster nodes can reach but GHCR isn't):

make images IMAGE_PREFIX=registry.example.com/orbital TAG=0.1.0
make push-images IMAGE_PREFIX=registry.example.com/orbital TAG=0.1.0

This produces control-plane (FastAPI + reconciler) and console (Next.js UI) images. Use your own checkout of deploy/chart/orbital in the steps below instead of the pulled chart.

3. Configure values

Create my-values.yaml:

image:
  controlPlane:
    repository: registry.example.com/orbital/control-plane
    tag: 0.1.0
  console:
    repository: registry.example.com/orbital/console
    tag: 0.1.0

apps:
  domain: apps.example.com          # *.apps.example.com -> ingress
ingress:
  className: nginx
  consoleHost: orbital.example.com
  tls:
    enabled: true
    secretName: streamlit-tls
console:
  url: https://orbital.example.com

registry:
  pushUrl: registry.example.com
  pullPrefix: registry.example.com

database:
  url: postgresql+psycopg://streamlit:***@db.example.com/streamlit  # empty = SQLite PVC

auth:
  console:
    enabled: true
    issuerUrl: https://idp.example.com/realms/main
    clientId: orbital
    clientSecret: "***"
    adminGroups: ["platform-admins"]
    creatorGroups: ["developers"]
    viewerGroups: ["everyone"]
  viewer:                            # private-app viewer auth (oauth2-proxy)
    enabled: true
    oauth2ProxyAuthUrl: http://oauth2-proxy.auth.svc/oauth2/auth

OIDC client requirements

Register a confidential client at your IdP with:

  • Redirect URIs: https://<consoleHost>/api/auth/callback (console login) and your oauth2-proxy callback if you use private apps.
  • Post-logout redirect URI: https://<consoleHost>/ (RP-initiated logout).
  • A groups claim in the ID token (e.g. Keycloak "Group Membership" mapper, claim name groups, full path off). RBAC is entirely group-based.
  • (optional, Keycloak) to let the console's group pickers list the realm's groups (auth.console.groupsFromKeycloak=true): enable service accounts on the client and grant its service account the query-groups and view-users roles of the realm's realm-management client. Otherwise provide suggestions statically via auth.console.knownGroups.

Required secrets. With console auth enabled, both auth.console.clientSecret (or existingSecret) and auth.console.sessionSecret are non-optional: the session secret signs the cookie that carries the caller's identity and role, so a guessable value is a full authentication bypass. The chart fails at helm install/upgrade if clientSecret is missing, or if you set sessionSecret explicitly to fewer than 32 characters; leave sessionSecret unset and the chart auto-generates a strong one. The control plane also refuses to start (whether or not it was launched via this chart) if ORBITAL_SESSION_SECRET is left at its insecure built-in default or is otherwise too short.

Private apps (viewer auth)

Deploy oauth2-proxy against the same IdP with --set-xauthrequest and a cookie domain covering .<apps-domain>, then set auth.viewer.*. deploy/auth/auth-stack.yaml.tmpl is a working demo (Keycloak + oauth2-proxy) to copy from.

4. Install

helm install orbital deploy/chart/orbital \
  -n orbital-platform --create-namespace \
  -f my-values.yaml

(use orbital instead of deploy/chart/orbital if you pulled the chart per step 2 rather than working from a checkout of this repo)

The chart creates the streamlit-apps, streamlit-builds and streamlit-scans namespaces, RBAC (the control plane can only touch those three namespaces), NetworkPolicies isolating them from each other and from cloud metadata endpoints (see above), the control plane + console Deployments and the console ingress. A post-install job builds the app base images (one per entry in baseImages.pythonVersions) with BuildKit and pushes them to your registry.

Verify:

kubectl -n orbital-platform get pods       # control-plane + console Running
kubectl -n streamlit-builds get jobs          # base image job(s) Complete

Open https://<consoleHost>, sign in, and deploy a first app (see USER.md).

5. Upgrades

helm upgrade orbital deploy/chart/orbital -n orbital-platform -f my-values.yaml
  • Always bump the image tag when releasing new platform images: with pullPolicy: IfNotPresent (the default), re-pushing the same tag will NOT be picked up by nodes that already cached it.
  • Base-image jobs re-run on every upgrade (baseImages.build=true); running apps keep their current images until their next rebuild.
  • With SQLite (default), the control plane is single-replica and upgrades cause a short API outage; running apps are unaffected. Use PostgreSQL for zero-downtime control-plane restarts.

6. Values reference (excerpt)

Value Default Purpose
apps.routing subdomain subdomain (<slug>.<domain>, wildcard DNS) or path (<domain>/app/<slug>, single host — for environments where wildcard DNS is unavailable)
apps.pathPrefix /app URL prefix in path mode
apps.domain apps.example.com apps domain (wildcard in subdomain mode; single host in path mode)
apps.urlPort 80 port in generated app URLs (tunnel setups)
apps.resources.* 250m/1 CPU, 512Mi/2Gi per-app requests/limits
builds.rootless true rootless vs privileged BuildKit
registry.pushUrl / pullPrefix minikube addon app image registry
registry.internal.enabled false dev-only in-cluster registry
registry.tls.enabled false TLS for build-pod↔registry traffic (see ADMIN.md#registry-tls) — unaffected/off by default so this quick-start needs no extra setup
registry.tls.existingSecret "" bring-your-own CA/cert/key Secret (keys tls.crt/tls.key/ca.crt); required when registry.tls.enabled=true and registry.internal.enabled=false
baseImages.pythonVersions ["3.12"] supported Python versions
database.url "" (SQLite PVC) PostgreSQL URL for production
auth.console.* disabled console OIDC login + group RBAC
auth.viewer.* disabled private-app viewer auth
controlPlane.reconcileInterval 3 reconcile loop period (s)
networkPolicy.enabled true default-deny NetworkPolicies for apps/builds/scans/control-plane namespaces (needs a NetworkPolicy-enforcing CNI)
networkPolicy.ingressNamespaceSelector {} (deny all) must be set to your ingress controller's namespace selector, or ingress is denied entirely
networkPolicy.apps.egressAllowlist [] extra CIDRs apps may reach beyond DNS (see ADMIN.md)

7. Uninstall

helm uninstall orbital -n orbital-platform
kubectl delete ns streamlit-apps streamlit-builds streamlit-scans   # removes all hosted apps!