> ## 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.

# 在 Kubernetes 上部署

> 用 kustomize 把控制台部进集群，以及 KubeRay 执行器依赖的那个 RWX 卷。

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
kubectl apply -k deploy/k8s/
kubectl -n starforge rollout status deploy/starforge-console
```

`deploy/k8s/` 是一套 kustomize overlay，覆盖命名空间、配置、密钥、RBAC、Deployment、
Service、Ingress 和两个 PVC。

## 它会创建什么

| 清单                            | 用途                                                |
| ----------------------------- | ------------------------------------------------- |
| `namespace.yaml`              | `starforge`                                       |
| `configmap.yaml`              | 非敏感的 `FORGE_*` 设置                                 |
| `secret.example.yaml`         | 敏感设置——**上生产前请替换来源**                               |
| `rbac.yaml`                   | `kuberay` 执行器需要的 RayJob 权限。没有它下发会 403             |
| `deployment.yaml`             | 单副本，`/healthz` 探针，CPU `250m`–`2`，内存 `512Mi`–`2Gi` |
| `service.yaml`、`ingress.yaml` | 集群内地址与外部路由                                        |
| `pvc.yaml`                    | 存储根的 PVC，加一个可选的本地 SQLite PVC                      |

apply 之前先把镜像指向你的仓库：

```yaml deploy/k8s/kustomization.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
images:
  - name: registry.company.com/forge/starforge-console
    newTag: "0.3.0"
```

## 真正要紧的是那个存储卷

```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
accessModes: [ReadWriteMany]
resources:
  requests:
    storage: 2Ti
storageClassName: nfs-client     # 必须是真的支持 RWX 的 StorageClass
```

<Warning>
  **ReadWriteMany 不是可选项，而 RWO 是静默失败的。** RWO 的 claim 不会报错——
  Kubernetes 会给每个 Pod 各自一个卷。多节点作业于是把 checkpoint 分片散落在不同 Pod 上，
  这次 run 再也续不了，而日志里没有任何东西说明原因。

  执行器的健康检查会把 `FORGE_K8S_STORAGE_SHARED` 和 claim 的实际 `accessModes` 核对。
  首次部署后去读那个结果，别想当然。
</Warning>

控制台把这个 claim 挂在 `FORGE_STORAGE_ROOT` 指定的那个路径上——
它往 `runs/<user>/<exp>/<run_id>/work` 里放作业目录、测量磁盘压力、执行回收。
训练 Pod 挂载同一个 claim，所以控制台写的路径就是作业打开的路径。

容量按「权重缓存 + 数据集缓存 + 所有 run 目录」估。2Ti 是一个起点而不是建议值；
光是权重缓存，每个基座模型就是几十 GB。

## 推荐配置

```yaml deploy/k8s/configmap.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
FORGE_DEFAULT_FLEET_KIND: "kuberay"
FORGE_STORAGE_ROOT: "/starforge"
FORGE_K8S_STORAGE_PVC: "starforge-storage"
FORGE_K8S_STORAGE_SHARED: "true"
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"
FORGE_JOB_RUNNER_MODE: "bundled"
FORGE_QUOTA_ENFORCE: "1"
```

其中四项值得理解而不是照抄：

<AccordionGroup>
  <Accordion title="FORGE_INGEST_URL 用的是集群内服务名" icon="network">
    训练 Pod 往它回传。用外部 ingress 地址会先出集群再绕回来；服务 DNS 名不会。
    绝不能填 `127.0.0.1`——那是 worker 自己的回环。
  </Accordion>

  <Accordion title="FORGE_KUBERAY_RAY_VERSION 必须和训练镜像一致" icon="triangle-alert">
    不一致的症状是「集群起来了但 worker 注册不上」，而唯一的线索是一行很容易淹没在启动输出里的版本警告。
  </Accordion>

  <Accordion title="FORGE_K8S_SHM_SIZE，因为容器默认只有 64MB" icon="cpu">
    Ray 的 object store 就在 `/dev/shm` 里。用默认值的话，稍大一点的 batch 立刻 OOM。
    它由内存支撑，所以它加上内存上限不能超过物理内存。
  </Accordion>

  <Accordion title="FORGE_KUBERAY_PRERUNNING_DEADLINE_S 给卡住的作业一个上限" icon="clock">
    没有它，拉不到的镜像或者没有节点满足的 nodeSelector 会让作业无限期 Pending，同时一直占着队列位置。
  </Accordion>
</AccordionGroup>

## 密钥

`secret.example.yaml` 只是用来说明结构。上生产前请换成真实来源——
`kubectl create secret`、Sealed Secrets 或 External Secrets Operator——
并把它从 `kustomization.yaml` 里移除。

至少要有：`FORGE_WEB_JWT_SECRET`、`FORGE_DB_URL`、`FORGE_REDIS_URL`、
`FORGE_S3_SECRET_KEY`、`FORGE_SECRET_ENC_KEY`，以及 OIDC 的 client secret。

## 副本数

清单里是单副本，这是安全的默认值。往上扩之前：

* `FORGE_DB_URL` 必须指向 Postgres。SQLite 是单写的
* `FORGE_WEB_JWT_SECRET` 必须固定，否则副本之间互相拒绝对方签的 token
* `FORGE_REDIS_URL` 必须配置，否则**后台角色会整个停掉**而不是跨副本不安全地跑——
  存储记账、诊断、看门狗和每日日报全都不再运行
* 把后台角色放进它自己的 Pod。设 `FORGE_INPROCESS_WORKERS=0`，再应用 worker Deployment——kustomize 路径下是
  `worker.yaml`，Helm 下是 `worker.enabled=true`。诊断与日报的 tick 会调 LLM 并一次占住事件循环好几秒；
  拆出去之后它们不再抬高 API 尾延迟，控制台副本也变成无状态的。副本数保持 1 是对的：每个角色都经 Redis
  选主，第二个副本只会空转

## 确认成功

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
kubectl -n starforge get pods
kubectl -n starforge port-forward svc/starforge-console 8080:80
curl -s localhost:8080/api/version
```

然后看执行器自己的视角——它才能告诉你存储卷是不是真的 RWX：

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
curl -s localhost:8080/api/cluster/health | jq .storage
```
