- Chart the chart version listed for your release (for example
0.3.104· appv1.19.95, the GA release when this page was last revised) - Helm 3.8+ · Kubernetes 1.23+ ·
craneor Docker (only if you mirror images) - Backend 1 × StatefulSet (single replica)
- Port
8080— serves GUI, API, and the terminal WebSocket - Sandbox mode opt-in
For everything after the install — upgrade, rollback, backup and restore, secrets,
user offboarding, scaling, teardown, and the full failure-mode catalogue — see the
Genesis Operations Runbook. It is
cloud-agnostic, and it is the page to hand the customer’s operations team at handover.
This is the full base-plus-sandbox-terminal install runbook. For the separate
sandbox_runner.runtime: kubernetes per-thread runner (one Pod per selected chat
thread), see Deploying the Genesis Sandbox Runner on GKE.What you’re installing
Two container images, one Helm release, one reachable URL. Genesis runs as a single-replica StatefulSet — a FastAPI backend serving the React web console, the REST API, and the terminal WebSocket, all on one port (8080). There is no separate service or port for the terminal.
When a user opens a terminal, the backend creates a workspace Pod for that user
in a dedicated namespace, attaches a tmux session, and streams it over the same
port. That is the on-demand sandbox mode — off by default, enabled in Phase 5.
Honest scope. The base install (Phases 1–4) is standard Helm. The in-pod coding
agent mode (Phase 5) is opt-in and is enabled by one
cliTerminal.enabled chart
setting, which renders the namespace, RBAC and quota as part of the release.Phase 1 — Preflight and grants
Run this before installing. Anything red here is a grant to request from your GCP admin — the checks are exact so nothing surfaces mid-deploy. Set your targets once; the checks reference them:If you do use Autopilot, three things it does not manage away. Compute Engine
quota still applies — Autopilot nodes are ordinary VMs, and a cluster that fails
minutes in with
STATUS: ERROR usually names SSD_TOTAL_GB only in
gcloud container clusters describe … --format='value(conditions)'. Requests are
rewritten — Autopilot enforces a memory-to-CPU ratio and raises CPU to match (a
workspace asking 500m/4Gi is admitted at roughly 616m) and adds ephemeral storage to
every container, so budget the admitted figures, not the requested ones. Nodes are
consolidated on Autopilot’s schedule, so expect occasional unscheduled backend
restarts and slower first workspace launches while a node provisions.get-credentials below):
Grants to request
Capacity formula
Genesis reserves the backend once, then one Pod and two volumes per sandbox user. CPU and memory scale with concurrent users; disk scales with lifetime users, because a workspace volume outlives its Pod so that a user’s saved conversations survive being reaped. Nothing reclaims a volume automatically.Nconcurrent is capped by cliTerminal.maxWorkspaces (default 25) and
Nlifetime by cliTerminal.maxLifetimeUsers (default 50). Lower
maxWorkspaces to what this cluster can actually hold — it is one value, and the
chart writes it to both the namespace quota and the backend’s own cap. A count quota
reserves nothing, so the disk ceiling costs only what people actually claim.
External prerequisites (you supply)
- An OIDC / Auth0 app — issuer, audience, JWKS URL, SPA client ID, and two
callback URLs (Phase 4). Verify JWKS reachable with
curl. - Your Databricks LLM — a workspace URL + a token, with the chat model published
as a serving endpoint. Verify:
curl -H "Authorization: Bearer $DAPI" <workspace>/api/2.0/serving-endpoints.
Professional Services checklist (operator responsibilities, not customer infra)
Four items that are nobody’s infrastructure and are therefore the ones that get skipped. Each has an owner and a verification, and none of them is done by the chart.Gate 1 — do not create anything until all of these hold. Every one is cheap now
and expensive later; the node-egress row alone is the difference between a one-hour
install and a one-day one.
kubectl get pods -n kube-system→ every podRunninginside ~1 minute (or, with no cluster yet, a0.0.0.0/0route in$NETWORKcarrying$NODE_TAG, or a Cloud NAT covering the node subnet, or Private Google Access on it)kubectl auth can-i create rolebindings -n genesis-cli→yesgcloud services list --enabledlistscontainerandcomputekubectl get storageclass→ exactly one row marked(default)- The node service account is the one holding
roles/artifactregistry.reader CPUSandSSD_TOTAL_GBheadroom exceeds the capacity formula- The JWKS probe prints
200 - All four Professional Services checklist rows are done, including the escrow owner in writing
Phase 2 — Get the software
Both images and the chart live in one private registry, already granted to databricks.com.Access — already granted to databricks.com. The
databricks.com Google
Workspace domain holds roles/artifactregistry.reader on
genesis-471800/genesis-dist — which covers the two images and the Helm chart
(same repo). Any databricks.com identity can pull; nothing is public.latest, main) for the backend. Prefer the
release tag: it names one build, so two installs of “the same release” are the same
deployment. If you do deploy a floating tag, read the image.pullPolicy warning in
Phase 3 first.
Node-SA pull — already set up for the dev cluster. A GKE node pulls images as
its own service account, not a databricks.com identity. For the dev cluster this is
already handled: Genesis granted your project’s default compute SA
roles/artifactregistry.reader on genesis-dist, so nodes pull both images and the
chart cross-project with no mirror. Point the chart straight at genesis-dist
(Phase 3) and go.Optional — mirror into your own AR. For a longer-lived or air-gapped cluster you
may prefer a local copy; then set Then copy with Mirror both images — the sandbox image is not part of the backend image, and it is
not pulled until the first user opens a terminal, so a missing mirror surfaces days
later. Coding sandbox (CLI Terminal) image covers
pinning it.No There is no
image.repository / GENESIS_CLI_TERMINAL_IMAGE
to your mirror path.Create the repo and set up auth once. gcloud auth configure-docker installs the
credential helper that both crane and docker read, so it is the only auth step
either recipe needs — but name both hosts, since a copy reads from us-central1 and
writes to your region:crane — it streams
registry-to-registry, so it needs no local Docker daemon, pulls nothing to disk, and
keeps the source digest and every platform in the manifest:crane? Docker does the same job by round-tripping through your machine. Pin the
platform — an unpinned pull on an Apple-silicon laptop mirrors an arm64 image that GKE
nodes cannot run, and the failure surfaces later as a crash-looping pod, not a bad copy:gcloud artifacts docker images copy — that subcommand does not exist, so
reach for one of the two recipes above rather than the gcloud verb you might expect.Pull the chart
Pin the chart version, and read the current one rather than trusting this page.
This runbook was last revised against chart Then pin what you read, and write it into the handover document.
0.3.104 / app v1.19.95. Omitting
--version installs whatever is newest, which makes two installs of “the same
runbook” different deployments. Ask the registry what exists:Gate 2 — do not install until all three hold:
helm show chart …prints aversion:and anappVersion:— your registry login works and the chart is reachable- Both images resolve in the registry your nodes will pull from:
crane manifest <ref> > /dev/null && echo OKfor the backend and the workspace image bash /tmp/genesis-chart/genesis/validate-retention.shexits0and ends withRetention contract holds in the rendered manifest.
bash,
because files inside a Helm tarball are not extracted executable.Phase 3 — Install the base
A values file, a master key, onehelm upgrade --install.
Create my-values.yaml. Replace every highlighted value; use your mirror path if you
mirrored.
Create the namespace, then install without --create-namespace
The chart renders its own Namespace object — named by the chart’s namespace
value, genesis by default — and marks it helm.sh/resource-policy: keep, so that a
helm uninstall or a later chart change can never cascade a delete to the data volume.
Two consequences:
helm install --create-namespacefails: the namespace Helm creates for the release collides with the one the chart renders.- A plain
kubectl create namespacealso fails on install: Helm refuses to take over an object it does not own.
--create-namespace:
genesis), the -n namespace and the chart’s namespace value must
all agree with those annotations. If you use a different namespace, set namespace: in
my-values.yaml to the same name and annotate that namespace instead.
Optional — backend scheduling priority. Off by default; turn it on only on a cluster
dedicated to Genesis. By default the backend runs at Kubernetes’ implicit priority 0,
the same as the workspace and agent Pods it creates, so on a full cluster the scheduler
has no reason to prefer it over the workloads that depend on it. On a cluster that runs
only Genesis, add:Creating the class needs rights on
scheduling.k8s.io PriorityClasses. Leave it off on
a shared cluster: a class above the default makes Genesis a preferred survivor over
other teams’ workloads, and changing preemptionPolicy to PreemptLowerPriority would
let it evict them. To reference a class your platform team already defines, set
priorityClass.name and keep create: false. It does not rank above Kubernetes’ own
system tiers, and is not meant to.First boot takes up to 10 minutes. The startup probe allows a long grace
(
failureThreshold: 60 × periodSeconds: 10) while the backend builds its DB, warms
sessions, and loads the LLM. Watch kubectl -n genesis get pod genesis-0 -w. To tell
“still working” from “stuck”, read the progress rather than the pod:Gate 3 — the base is installed when all four hold:Then prove the Pod runs the build the tag names now — the check that catches a
floating tag served from a stale node cache:
ImagePullBackOff here is the node-SA grant or a tag that is not in the registry;
Pending on the PVC is the StorageClass row from Gate 1. A MISMATCH on a floating tag
is image.pullPolicy left at IfNotPresent; set it to Always and delete the Pod so it
is recreated. On a rolling upgrade, also make sure you are reading the new Pod, not
the old one that is still terminating.Phase 4 — Access and sign-in
One port serves everything; both surfaces authenticate through the same OIDC app.8080— the only port: GUI, API, terminal WS- 1 URL — browser and any client hit the same host
- 2 callbacks — GUI origin + a loopback, in the OIDC app
https://<your-host>/ and sign in. The OIDC app must list your host in
Allowed Callback URLs and Web Origins, and AUTH0_CLIENT_ID must be set (the
console reads it from /api/config/frontend). Register both callbacks:
No public ingress? Port-forward. For a private sandbox, skip the Ingress and
reach both surfaces locally:
Gate 4 — access works when all three hold:
curl -sS -o /dev/null -w '%{http_code}\n' https://<your-host>/api/healthcheck→200from outside the cluster. A302here is the ingress auth annotation capturing the probe — enableingress.authExemptHealthrather than removing the auth.- A browser at
https://<your-host>/reaches the sign-in page and a real user completes the round trip back to the console. - That user sends a chat message and gets a reply. Without a reachable LLM the server starts fine and thread creation fails with “Bot not found or unavailable” — which is an LLM-configuration answer, not a deployment one.
AUTH0_CLIENT_ID is unset — the console reads it from
/api/config/frontend.Phase 5 — Enable the sandbox terminal
cliTerminal.enabled=true is the whole switch: it renders the cluster objects the
backend needs and turns the feature on in the application. Move 2 covers the cluster
objects; Move 1 is the sizing and image settings the chart does not decide for you.
Read the image section first — it is the setting most often wrong.
Coding sandbox (CLI Terminal) image
The coding sandbox — the per-user workspace Pod behind the CLI Terminal — runs a separate image from the main Genesis image. Upgrading, mirroring or pinning the backend does nothing for it: it has to be mirrored and pinned in its own right.-
The feature is off by default. Nothing pulls the sandbox image until
cliTerminal.enabled=trueand a user opens a terminal — which is why a missing or wrong image surfaces with the first user, not at install. -
Where to get it. Use the sandbox image from the registry Genesis provided you — for
this install,
genesis-dist/genesis-sandbox-claude— or your mirror of it. The same image is also published publicly on Docker Hub asdocker.io/genesiscomputingai/genesis-sandbox-claude, pullable without credentials, with the same weeklylatestand dated tags. It contains the coding tools only, no Genesis server code, so it can serve clusters that have no access to a private registry. -
The reference must be fully qualified. On Kubernetes,
cliTerminal.image(the app settingcli_terminal.image, envGENESIS_CLI_TERMINAL_IMAGE) must name the registry host, repository and tag, in a registry your nodes can pull from:us-central1-docker.pkg.dev/<project>/<repo>/genesis-sandbox-claude:<tag>ordocker.io/genesiscomputingai/genesis-sandbox-claude:<tag>. The application’s built-in default is an unqualified name, which works for a local Docker install and cannot work here: Kubernetes resolves an unqualified name against Docker Hub’slibrary/namespace, where the image does not exist, and every workspace Pod sits inImagePullBackOff. -
Pin it.
genesis-sandbox-claude:latestis rebuilt and refreshed weekly, and each build is also published under a dated, immutable tag (for example20260923-35908082637-1). Pin one of the dated tags — list them withcrane ls $SRC/genesis-sandbox-claude— and record its digest beside the backend’s. Workspace Pods are created with Kubernetes’ default pull policy, so a re-pointed tag other thanlatestis not re-pulled on a node that already has it cached. -
Mirror it multi-arch, with
crane.crane cpcopies registry-to-registry and keeps every platform in the manifest. Adocker pull/docker pushround trip from a laptop can publish only the laptop’s architecture — on Apple silicon, anarm64-only image thatamd64GKE nodes cannot run:
cliTerminal.image in the
values file, or GENESIS_CLI_TERMINAL_IMAGE in config.extraEnv (Move 1) with
cliTerminal.image left empty. The warning under Move 1 explains which wins.
Move 1 — the settings the chart does not decide
Add toconfig.extraEnv in my-values.yaml and re-run helm upgrade (use your
mirror path for the image if you mirrored):
Request and limit are different numbers on purpose.
MEMORY is the ceiling a
workspace may grow into and the point at which Kubernetes OOM-kills it — which kills
every session that user has open. MEMORY_REQUEST is what the scheduler reserves, and
so what cluster capacity must actually supply: 10 concurrent users need 10 × the
request, not 10 × the ceiling. Setting them equal makes every idle workspace hold the
full 8Gi, which is what made this mode need a specially sized cluster.Turning this on does not require a Helm restart. Two delivery paths reach the same
settings — pick by whether you want the state in your values file or in the running
server:
- IaC (what this step shows). Values in
config.extraEnv/genesisConfig.content, applied withhelm upgrade. Best for a reproducible deployment: the enablement lives in the values file and survives a reinstall. - In-flight. On a running server an admin sets the same settings in Config →
Advanced Settings, and they take effect on the next terminal attach — no restart,
no rollout. Every
cli_terminal.*field is declaredenv_leaf: truein the config schema: a panel write refreshes that field’s environment variable immediately, and the attach path reads the variable at call time rather than caching it at startup.
enabled in Advanced Settings without them still
403s, exactly as it would from the values file.cliTerminal.enabled=true is the chart’s platform switch: it pins the effective
cli_terminal.enabled, cli_terminal.runtime, and cli_terminal.k8s_namespace values to
the rendered Kubernetes objects. Setting it to false removes those chart-managed objects
from the release, but deliberately does not force the application’s own setting to false;
deployments that provision the namespace and RBAC outside this chart can keep working.What a live change reaches. image and the CPU / memory / volume sizes apply to
workspace Pods created after the change — a user with a running workspace keeps their
current Pod until it is reaped and recreated, so nothing interrupts an agent mid-task.
runtime and k8s_namespace are the exception: both are fixed for the life of the
server process, and a live change to either is logged and ignored rather than applied,
because the workspaces already created live on the old backend and in the old namespace
and nothing else can find them to reap. Change those from the values file, and retire the
existing workspaces first.Move 2 — grant the cluster objects
cliTerminal.enabled=true in your values file renders the namespace, RBAC, quota and
(optionally) the workspace NetworkPolicy as part of the release, and sets plus force-locks
GENESIS_CLI_TERMINAL_ENABLED, _RUNTIME and _K8S_NAMESPACE on the backend for you.
This one flag is what the old kubectl apply step and the enablement env vars used to be
between them:
default unless
serviceAccount.create or serviceAccount.name is set — so there is nothing to edit for
the common case.
This namespace survives A separate script from
helm uninstall, and survives turning the feature back off.
It holds every user’s home volume and therefore their saved Claude Code conversations, so
it carries helm.sh/resource-policy: keep. Without that, an ordinary helm upgrade
setting cliTerminal.enabled: false would take it out of the release manifest and
Kubernetes would cascade the delete to everyone’s work — an enablement flag doubling as a
data-destruction flag. Disabling revokes the RBAC and releases the quota, nothing more.
A chart-created workspace namespace is labeled with the Genesis namespace of the backend
instance that owns it; the cleanup scripts use that label rather than a release’s computed
values, which prevents one instance from draining another instance’s retained workspaces.Removal is deliberate and separate: cluster/scripts/teardown.sh <name> --delete-workspaces, which asks before it does it. To retire one departed user instead of
everyone, see
Operations Runbook › Offboarding.
Use
cliTerminal.createNamespace: false when the namespace already exists or is managed
elsewhere — that flag is about creation, not survival.Disabling is access-only. The workspace Pods are retained with the namespace, and
enabled: false removes the reaper that used to remove them along with the RBAC that let
it act — so an agent mid-task keeps running. Drain first, which keeps every volume:teardown.sh on purpose: it leaves the release running, which is
what makes it safe to run before you disable.If your platform objects are applied outside Helm, render them from the same
template rather than keeping a hand-edited copy — a second copy diverges silently,
because only one of the two ever gets updated:
Pending and
every attach fails, which reads as a broken feature rather than a starved one. Lower
cliTerminal.maxWorkspaces to what the cluster holds — one value, which the chart writes
to both the namespace quota and the backend’s own cap. Sizing is in the
capacity formula above; the scheduling traps that do not look like
capacity problems — zone-pinned volumes, a node pool refused for disk quota rather than
CPU — are in
Operations Runbook › Scaling.
Gate 5 — the terminal is genuinely on when all four hold. Skip any of these and
the feature fails for the first real user rather than for you:Read all three identities off the live deployment first. The workspace namespace and
the backend’s service account are both configurable, so a gate that hardcodes them
checks a different deployment than the one you installed — and on a cluster with more
than one Genesis instance, possibly somebody else’s:An empty
$WS_NS means the chart pinned nothing, which means cliTerminal.enabled is
not actually true in the release you installed — fix that before reading the rest of
this gate.- Attach a real terminal as a real user, from the console. The first attach pulls the ~700 MB workspace image and can take up to a minute; after that it is instant. Nothing short of a shell prompt counts — a rendered tab is not an attached session.
use_cli_terminal permission, and a Kubernetes 403 after the socket attaches is the
namespace or RBAC. The full symptom → cause → fix table is in the
Operations Runbook.Phase 6 — Final verification
Confirm the deploy by content, then hand the console to a user.Gate 6 — the deployment is ready to hand over when all six hold. Five of these
are cheap; the fifth is the one installs skip, and it is the one that silently ships a
broken deployment.
-
Health, from outside. Expect
200: -
No stuck pulls, in either namespace.
kubectl get pods -n genesisandkubectl get pods -n genesis-cli— nothing inImagePullBackOff,CrashLoopBackOfforPending. A stuck pull here is the node-SA row from Gate 1 or a tag that is not in the registry. -
Console.
https://<host>/signs a real user in, and that user gets a reply to a chat message. Admins also get Pulse → CLI Workspaces (/pulse/cli-workspaces) to list running workspace Pods with restart and cancel. - Terminal. A user opens a terminal and gets a shell prompt. The first attach pulls the ~700 MB image and can take a minute; after that it is instant. The Pod is reaped when idle and the volume persists, so reconnecting restores their work.
-
Embeddings, verified separately from chat. Chat replying proves nothing about
semantic search — they are different routes and often different endpoints and
credentials. Force a real embedding call:
"success": falsecarries the provider’s own error text. Do not sign off on a status chip that merely is not red. -
Recorded. The chart version and the running image digest are written into
the handover document, along with the escrow owner for
GENESIS_MASTER_KEY.
When it goes wrong
The failure-mode catalogue lives in one place — symptom → cause → fix for every failure below, in Operations Runbook › Incident triage. It is not repeated here, because a description kept in two places goes stale in one of them. What this page keeps is the GCP-specific remedy detail each symptom points at. Skim this index before you start; each row names the phase where the cause is fixed.
Two facts that are not symptoms but are worth knowing before you tear anything down:
- PVCs are retained on uninstall, by design. The backend volume and every workspace
volume survive
helm uninstall; workspace volumes are per-user home directories and nothing auto-deletes them. Reclaiming them is deliberate and explicit — see Teardown without data loss. - Chart version matters for the retention contract. Helm reads the retention policy
from the live object, so it protects nothing until an upgrade has written it. On a
deployment that enabled the terminal on a chart older than
0.3.83, going straight tocliTerminal.enabled: falsein onehelm upgradestill deletes the namespace. Upgrade with the feature still on first, then disable in a second step. Deployments enabling it for the first time on0.3.83or later are never exposed to this.
Connecting remotely to the session
To watch or drive a Claude Code session managing this cluster from another device, or to coordinate two sessions inside the same pod, see Remote Connections to Claude Code — including the caveat that Remote Control does not work with the in-podclaude
that routes its LLM through the Genesis proxy.
Genesis on GKE · last revised against chart
0.3.104 / app v1.19.95 — use the chart
version listed for your release, confirm it with helm show chart, and pin what you read ·
images + chart in
us-central1-docker.pkg.dev/genesis-471800/genesis-dist. Replace every highlighted
placeholder with your values. Preflight (Phase 1) is safe to run against any cluster
before you commit. Day-2 operations:
Genesis Operations Runbook.
