> ## Documentation Index
> Fetch the complete documentation index at: https://starforge.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Install with Helm

> The same Kubernetes deployment, values-driven — for several environments or a GitOps pipeline.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
helm install starforge deploy/helm/starforge-console \
  --namespace starforge --create-namespace \
  --set image.repository=registry.company.com/forge/starforge-console \
  --set config.FORGE_INGEST_URL=http://starforge-console.starforge.svc.cluster.local \
  --set secrets.existingSecret=starforge-console-secrets
```

The chart renders the same objects as [the kustomize manifests](/en/ops/install-kubernetes):
ServiceAccount, ConfigMap, PVC, Role, RoleBinding, Service, Deployment, and optionally an Ingress
and a Secret.

Pick the chart when you run several environments or drive deployment from values. Pick the manifests
when you want to read exactly what will be applied.

## A values file worth starting from

```yaml values.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
image:
  repository: registry.company.com/forge/starforge-console
  tag: "0.3.0"
imagePullSecrets:
  - name: starforge-registry

storage:
  accessMode: ReadWriteMany     # not negotiable, see below
  size: 2Ti
  storageClassName: nfs-client

config:
  FORGE_DEFAULT_FLEET_KIND: kuberay
  FORGE_STORAGE_ROOT: /starforge
  FORGE_PUBLIC_URL: https://starforge.company.com
  FORGE_INGEST_URL: http://starforge-console.starforge.svc.cluster.local
  FORGE_KUBERAY_RAY_VERSION: "2.55.1"
  FORGE_K8S_SHM_SIZE: 64Gi
  FORGE_KUBERAY_PRERUNNING_DEADLINE_S: "1800"

secrets:
  existingSecret: starforge-console-secrets

ingress:
  enabled: true
  className: nginx
  host: starforge.company.com
  tls:
    enabled: true
    secretName: starforge-console-tls
```

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
helm upgrade --install starforge deploy/helm/starforge-console \
  -n starforge --create-namespace -f values.yaml
```

## Three values decide whether it works

<ParamField path="storage.accessMode" type="ReadWriteMany" required>
  An RWO claim **does not fail**. Kubernetes gives each pod its own volume, so a multi-node job's
  checkpoint shards split across pods and the run cannot be resumed — with nothing in the logs to
  say why. The chart defaults to RWX; the StorageClass has to actually support it.
</ParamField>

<ParamField path="config.FORGE_INGEST_URL" type="url" required>
  Training pods POST their metrics to it. Use the in-cluster service name. An external ingress
  address leaves the cluster and comes back in; `127.0.0.1` is the worker's own loopback.
</ParamField>

<ParamField path="secrets.existingSecret" type="string" required>
  Without `FORGE_WEB_JWT_SECRET` each process signs with its own key: every restart logs everyone
  out, and more than one replica cannot work at all. The chart's install notes warn when neither
  this nor `secrets.create` is set.
</ParamField>

## Secrets

`secrets.create: true` renders a Secret from `secrets.data` — convenient on a test cluster, wrong in
production, because the values land in your release history.

Point `secrets.existingSecret` at a Secret from `kubectl create secret`, Sealed Secrets, or the
External Secrets Operator:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
kubectl -n starforge create secret generic starforge-console-secrets \
  --from-literal=FORGE_WEB_JWT_SECRET="$(openssl rand -hex 32)" \
  --from-literal=FORGE_DB_URL='postgresql+psycopg://forge:...@postgres:5432/forge' \
  --from-literal=FORGE_REDIS_URL='redis://:...@redis:6379/0' \
  --from-literal=FORGE_S3_SECRET_KEY='...' \
  --from-literal=FORGE_SECRET_ENC_KEY='...'
```

## One thing the chart does for you

`FORGE_K8S_STORAGE_PVC` is filled in from the claim the chart creates, so the executor is always
told about the claim that actually exists. That pairing is easy to break by hand — the executor
checks a claim that is not the one mounted, reports healthy, and the mismatch only surfaces when a
multi-node run fails to resume.

Override it only when reusing a claim the chart does not own:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
helm install starforge deploy/helm/starforge-console --set storage.existingClaim=my-nfs-claim
```

## Verify before you apply

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
helm lint deploy/helm/starforge-console
helm template sf deploy/helm/starforge-console -f values.yaml | kubectl apply --dry-run=client -f -
```

## Confirm it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
kubectl -n starforge rollout status deploy/starforge-console
kubectl -n starforge get pvc -o custom-columns=NAME:.metadata.name,MODES:.spec.accessModes
kubectl -n starforge port-forward svc/starforge-console 8080:80
curl -s localhost:8080/api/cluster/health | jq .storage
```

That last call is the one worth reading: it compares the `FORGE_K8S_STORAGE_SHARED` declaration
against the claim's real access modes, which is the check that catches an RWO claim before a job
does.

<Note>
  The chart is verified with `helm lint` and `helm template` on every change. Installing it against a
  live cluster is not something CI here can do — no runner has one — so the first install on your
  cluster is the first real test. Run the dry-run above first.
</Note>
