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

# 部署到 Kubernetes

> 单文件生产环境 manifest，配套 CI workflow 自动构建一个镜像、推送到 Harbor 并回写镜像 tag。

Kubernetes 部署刻意做得最小化：一个 YAML 文件、一个辅助脚本、一个 CI workflow。
整套设计能在一屏内看完，从上往下读，后面想加东西也好扩展。

```
k8s/
├── README.md
└── prod/
    ├── k8s-prod.yaml         Namespace + Deployment + Service + Ingress
    ├── create-secrets.sh     从 .env.prod 生成 `vibestrap-secrets` Secret
    └── .env.prod             你的生产环境变量（gitignored，参考 .env.example）
```

## Manifest 里有什么

`k8s/vibestrap.yaml` 按这个顺序定义了四种资源：

1. **Namespace** —— `vibestrap`
2. **Deployment** —— Next.js 应用，1 副本，`/api/ping` 探针，合理的资源 requests/limits
3. **Service** —— ClusterIP，端口 80 → 3000
4. **Ingress** —— nginx + cert-manager TLS，内置 `www → apex` 跳转

只从 Harbor 拉一个镜像：`harbor.funkro.com/vibestrap/vibestrap`。集群里
**没有**任何迁移 Job——见下文 [数据库迁移](#数据库迁移)。

## CI 帮你做了什么

`.github/workflows/docker-build-push.yml` 在以下情况会跑：

* 推送到 `main` 分支
* 推送 `v*` 形式的 tag
* 在 GitHub Actions UI 手动触发

每次跑会：

<Steps>
  <Step title="构建镜像">
    Dockerfile 的 runner stage 打成
    `harbor.funkro.com/vibestrap/vibestrap:<version>`，
    同时额外打 `:latest` tag。
  </Step>

  <Step title="推送到 Harbor">
    用 GitHub Actions secrets 里的 `HARBOR_USERNAME` + `HARBOR_PASSWORD` 登录。
  </Step>

  <Step title="计算 version tag">
    Tag 推送用 git tag（比如 `v1.2.0`）。分支推送用 `main-<sha7>`。
    两种格式都是不可变的，不像 `:latest`。
  </Step>

  <Step title="自动回写 manifest">
    Workflow 把 `k8s/vibestrap.yaml` 里的 `image:` 行改成新版本，
    并以 `[skip ci]` 提交回 `main`。git 里的 manifest 永远跟仓库里的镜像
    版本一致。
  </Step>
</Steps>

<Note>
  CI 里**不放** `kubeconfig`。部署本身仍然是你手动敲命令 —— 这是有意为之。
</Note>

### GitHub Actions secrets

在 **Settings → Secrets and variables → Actions** 里加这两个：

| Secret            | 内容                             |
| ----------------- | ------------------------------ |
| `HARBOR_USERNAME` | 你的 Harbor 账号用户名                |
| `HARBOR_PASSWORD` | Harbor 密码或 robot account token |

## 首次部署

第一次 `kubectl apply` 之前，集群里要先准备好三样东西。

### 1. Harbor 拉取密钥

让集群能从你的私有仓库拉镜像：

```bash theme={null}
kubectl create namespace vibestrap
kubectl create secret docker-registry harbor-secret \
  --namespace vibestrap \
  --docker-server=harbor.funkro.com \
  --docker-username='<YOUR_HARBOR_USERNAME>' \
  --docker-password='<YOUR_HARBOR_PASSWORD>'
```

<Warning>
  K8s 的 pull secret 是 namespace 级隔离的 —— 放在 `default` 或任何其他
  namespace 的 `harbor-secret` 在 `vibestrap` namespace 里**用不了**。
  必须加 `--namespace vibestrap`。`ImagePullBackOff` 最常见的原因就是这个
  secret 建错了 namespace。
</Warning>

如果同一个集群已经为别的项目建过 `harbor-secret`，可以直接复制过来，不用
重新输密码：

```bash theme={null}
kubectl get secret harbor-secret -n <其他namespace> -o yaml \
  | sed '/namespace:/s/<其他namespace>/vibestrap/; /resourceVersion:\|uid:\|creationTimestamp:/d' \
  | kubectl apply -f -
```

### 2. 应用 Secret —— 只有两个字段必填

镜像启动时只校验 `DATABASE_URL` 和 `BETTER_AUTH_SECRET`。**其他环境变量
全部可选，留空时对应功能自动跳过**——你可以先把服务跑起来，之后想要哪个
功能再回来填，重跑脚本 + `kubectl rollout restart` 就行。

```bash theme={null}
cp .env.example k8s/.env
# 编辑 k8s/.env。只有这两行需要填真实值：
#   DATABASE_URL=postgres://user:pass@host:5432/db
#   BETTER_AUTH_SECRET=<openssl rand -base64 32>
# 其余字段需要哪个功能时再填。
./k8s/create-secrets.sh
```

辅助脚本会去掉注释、空行和值上意外加的引号（kubectl 会把引号当成值的一部分，
Better Auth 和 Stripe SDK 会爆），然后帮你跑
`kubectl create secret generic ... --from-env-file`。Deployment
通过 `envFrom: secretRef` 把所有变量一次性注入。

<Warning>
  `k8s/.env` 已 gitignore，别提交进去。新增或改动值之后重跑
  `create-secrets.sh` 即可。
</Warning>

### 3. ingress-nginx + cert-manager

Ingress 默认 `ingressClassName: nginx`，并依赖一个名为 `letsencrypt-prod`
的 ClusterIssuer。如果集群里还没装：

<CodeGroup>
  ```bash ingress-nginx theme={null}
  helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
  helm install ingress-nginx ingress-nginx/ingress-nginx \
    --namespace ingress-nginx --create-namespace
  ```

  ```bash cert-manager theme={null}
  helm repo add jetstack https://charts.jetstack.io
  helm install cert-manager jetstack/cert-manager \
    --namespace cert-manager --create-namespace \
    --set installCRDs=true
  ```
</CodeGroup>

然后照 [cert-manager 文档](https://cert-manager.io/docs/configuration/acme/)
建一个 `letsencrypt-prod` ClusterIssuer。

## 数据库迁移

迁移**不**在集群里跑。运行时镜像不带 `drizzle-kit`，也没有 Job 要照看。
取而代之，触动 schema 的发布之前，从本机跑：

```bash theme={null}
DATABASE_URL='postgres://user:pass@prod-host:5432/db?sslmode=require' \
  pnpm db:migrate
```

然后 `kubectl apply` 新镜像。单副本部署本身就没竞态窗口——schema 什么时候动
完全你说了算。

### 生产 DB 在私有 VPC 里怎么办

如果本机直接连不到生产 DB，在集群内拉一个一次性 pod，借集群网络跑同一条命令。
完整的 `kubectl run` 命令在 `k8s/README.md` 里；大致长这样：

```bash theme={null}
kubectl run vibestrap-migrate --rm -it --restart=Never \
  --namespace vibestrap \
  --image=node:22-alpine \
  --env="DATABASE_URL=$DATABASE_URL" \
  -- sh -c "cd /tmp && git clone <repo> app && cd app && \
            corepack enable && pnpm install && pnpm db:migrate"
```

这是兜底方案——只有真的从集群外连不上 DB 时才用。

## 部署

CI workflow 把 manifest 改好之后,每次部署是这样:

```bash theme={null}
git pull

# 仅当本次发布包含 schema 变更时
DATABASE_URL='postgres://...' pnpm db:migrate

kubectl apply -f k8s/vibestrap.yaml

# 看 rollout
kubectl rollout status deployment/vibestrap -n vibestrap
```

如果只是改了环境变量（没有新代码），刷新 Secret 然后滚一下 Deployment：

```bash theme={null}
./k8s/create-secrets.sh
kubectl rollout restart deployment/vibestrap -n vibestrap
```

## 长大之后怎么扩

Manifest 故意做得很薄 —— 真有需要再加。下面每一项都很小（10–30 行），
彼此独立。

| 加什么                         | 什么时候加                    | 加在哪                   |
| --------------------------- | ------------------------ | --------------------- |
| `HorizontalPodAutoscaler`   | 流量波动大，固定副本数浪费钱或扛不住       | `k8s/` 下新文件           |
| `PodDisruptionBudget`       | 跑 ≥3 副本且想要节点 drain 零停机   | 新文件                   |
| `NetworkPolicy`             | 多租户集群，想限制出站只到 DB 和外部 API | 新文件                   |
| `topologySpreadConstraints` | 多可用区集群，想容忍单可用区故障         | 内联在 Deployment spec 里 |
| 多环境                         | 真的不只一个环境时                | `k8s/staging/` 平行目录   |

## 故障排查

| 现象                                        | 可能原因                                                    | 检查方法                                                      |
| ----------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- |
| `ImagePullBackOff`                        | Harbor 凭证错了，或 `vibestrap` namespace 里没有 `harbor-secret` | `kubectl get secret harbor-secret -n vibestrap`           |
| 首次启动 Pod `CrashLoopBackOff`               | 缺必填 env（`DATABASE_URL`、`BETTER_AUTH_SECRET` 等）          | `kubectl logs <pod> -n vibestrap` —— Zod 会打印缺的 key        |
| Pod 起来了但 DB 查询报 "column does not exist"   | 上线新镜像之前忘了跑 `pnpm db:migrate`                            | 从本机跑迁移，然后重启 Deployment                                    |
| TLS 证书一直 pending                          | DNS 没指向 ingress LB，或 ClusterIssuer 缺失                   | `kubectl describe certificate vibestrap-tls -n vibestrap` |
| Stripe webhook 返回 400 "invalid signature" | `STRIPE_WEBHOOK_SECRET` 不对（live 和 test 串了）              | 从 Stripe 后台重新复制，刷新 secret，重启 Deployment                   |

## 官方文档

* [Kubernetes 文档](https://kubernetes.io/docs/)
* [cert-manager](https://cert-manager.io/docs/)
* [nginx-ingress](https://kubernetes.github.io/ingress-nginx/)
* [Harbor](https://goharbor.io/docs/)
