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
FORGE_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
FORGE_INGEST_URL uses the in-cluster service name
FORGE_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.FORGE_KUBERAY_RAY_VERSION must match the training image
FORGE_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.
FORGE_K8S_SHM_SIZE, because 64MB is the container default
FORGE_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.FORGE_KUBERAY_PRERUNNING_DEADLINE_S bounds a stuck job
FORGE_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: FORGE_WEB_JWT_SECRET, FORGE_DB_URL, FORGE_REDIS_URL, FORGE_S3_SECRET_KEY,
FORGE_SECRET_ENC_KEY, and the OIDC client secret.
Replicas
The manifest ships one replica, and that is the safe default. Before scaling up:FORGE_DB_URLmust point at Postgres. SQLite is single-writerFORGE_WEB_JWT_SECRETmust be fixed, or replicas reject each other’s tokensFORGE_REDIS_URLmust be set, or background roles stop entirely rather than run unsafely across replicas — storage accounting, diagnosis, the watchdog and the daily report all go quiet- Run the background roles in their own pod. Set
FORGE_INPROCESS_WORKERS=0and apply the worker Deployment —worker.yamlin the kustomize path,worker.enabled=truein the chart. The diagnosis and daily-report ticks call an LLM and hold the event loop for seconds at a time; out of process they stop inflating API tail latency, and the console’s replicas become stateless. One replica is correct: every role elects a leader through Redis, so a second one idles