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.Zandghcr.io/gggard/orbital/console:X.Y.Z - Helm chart:
oci://ghcr.io/gggard/orbital/charts/orbital, versionX.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 thequery-groupsandview-usersroles of the realm'srealm-managementclient. Otherwise provide suggestions statically viaauth.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!