deploy/k8s/ is a kustomize overlay covering the namespace, config, secrets, RBAC, deployment,
service, ingress and the two PVCs.
What it creates
Point the image at your registry before applying:
deploy/k8s/kustomization.yaml
The storage claim is the part that matters
TUNEPLANE_STORAGE_ROOT names — it stages job
directories into runs/<user>/<exp>/<run_id>/work, measures disk pressure, and reclaims. Training
pods mount the same claim, so a path the console writes is the path a job opens.
Size it for weight cache plus dataset cache plus every run directory. 2Ti is a starting point, not a
recommendation; the weight cache alone is tens of gigabytes per base model.
Recommended configuration
deploy/k8s/configmap.yaml
TUNEPLANE_INGEST_URL uses the in-cluster service name
TUNEPLANE_INGEST_URL uses the in-cluster service name
Training pods report back to it. The external ingress address would leave the cluster and come
back in; the service DNS name does not. Never
127.0.0.1 — that is the worker’s own loopback.TUNEPLANE_KUBERAY_RAY_VERSION must match the training image
TUNEPLANE_KUBERAY_RAY_VERSION must match the training image
A mismatch presents as “the cluster came up but workers never registered”, and the only clue is a
single version warning easily lost in startup output.
TUNEPLANE_K8S_SHM_SIZE, because 64MB is the container default
TUNEPLANE_K8S_SHM_SIZE, because 64MB is the container default
Ray’s object store lives in
/dev/shm. At the default, anything but a tiny batch OOMs
immediately. It is RAM-backed, so this plus the memory limit must stay under physical memory.TUNEPLANE_KUBERAY_PRERUNNING_DEADLINE_S bounds a stuck job
TUNEPLANE_KUBERAY_PRERUNNING_DEADLINE_S bounds a stuck job
Without it, an unpullable image or a nodeSelector nothing satisfies leaves a job Pending forever
while holding its queue slot.
Secrets
secret.example.yaml exists to show the shape. Replace it with a real source — kubectl create secret, Sealed Secrets, or the External Secrets Operator — and remove it from
kustomization.yaml before production.
At minimum: TUNEPLANE_WEB_JWT_SECRET, TUNEPLANE_DB_URL, TUNEPLANE_REDIS_URL, TUNEPLANE_S3_SECRET_KEY,
TUNEPLANE_SECRET_ENC_KEY, and the OIDC client secret.
Replicas
Use the same PostgreSQL database, Redis 6.2 or later, fixed JWT secret and shared storage across all API and worker pods. Enableworker.enabled=true in Helm;
it turns off API background loops and creates one scalable worker Deployment.
Every worker runs every coordination loop (scheduling, reconcile, watchdog)
under a leader lease, so exactly one replica acts per loop and the others stand
by, and each worker adds consumer capacity. Set worker.replicas=3 for three
workers. Kustomize includes worker.yaml; there is no separate controller.
TUNEPLANE_WORKER_CONCURRENCY controls in-flight deliveries per process (default
4). TUNEPLANE_WORKER_IO_CONCURRENCY limits imports and Volume measurements
across the entire pool (default 4). Set the same shared limit on every replica.
Increasing replicas alone does not raise that storage budget. Account for database
connections and storage bandwidth when raising it.
Use storage that supports conditional writes. Browser upload CORS must allow
If-None-Match alongside the existing upload headers. Upgrade API, consumers, SDK and
console together: uploaded Volume files require publication before becoming
visible. Managed reads use immutable generations; old mutable data directories
are not supported by this protocol.