跳转到主要内容
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 手动触发
每次跑会:
1

构建镜像

Dockerfile 的 runner stage 打成 harbor.funkro.com/vibestrap/vibestrap:<version>, 同时额外打 :latest tag。
2

推送到 Harbor

用 GitHub Actions secrets 里的 HARBOR_USERNAME + HARBOR_PASSWORD 登录。
3

计算 version tag

Tag 推送用 git tag(比如 v1.2.0)。分支推送用 main-<sha7>。 两种格式都是不可变的,不像 :latest
4

自动回写 manifest

Workflow 把 k8s/vibestrap.yaml 里的 image: 行改成新版本, 并以 [skip ci] 提交回 main。git 里的 manifest 永远跟仓库里的镜像 版本一致。
CI 里不放 kubeconfig。部署本身仍然是你手动敲命令 —— 这是有意为之。

GitHub Actions secrets

Settings → Secrets and variables → Actions 里加这两个:
Secret内容
HARBOR_USERNAME你的 Harbor 账号用户名
HARBOR_PASSWORDHarbor 密码或 robot account token

首次部署

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

1. Harbor 拉取密钥

让集群能从你的私有仓库拉镜像:
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>'
K8s 的 pull secret 是 namespace 级隔离的 —— 放在 default 或任何其他 namespace 的 harbor-secretvibestrap namespace 里用不了。 必须加 --namespace vibestrapImagePullBackOff 最常见的原因就是这个 secret 建错了 namespace。
如果同一个集群已经为别的项目建过 harbor-secret,可以直接复制过来,不用 重新输密码:
kubectl get secret harbor-secret -n <其他namespace> -o yaml \
  | sed '/namespace:/s/<其他namespace>/vibestrap/; /resourceVersion:\|uid:\|creationTimestamp:/d' \
  | kubectl apply -f -

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

镜像启动时只校验 DATABASE_URLBETTER_AUTH_SECRET其他环境变量 全部可选,留空时对应功能自动跳过——你可以先把服务跑起来,之后想要哪个 功能再回来填,重跑脚本 + kubectl rollout restart 就行。
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 把所有变量一次性注入。
k8s/.env 已 gitignore,别提交进去。新增或改动值之后重跑 create-secrets.sh 即可。

3. ingress-nginx + cert-manager

Ingress 默认 ingressClassName: nginx,并依赖一个名为 letsencrypt-prod 的 ClusterIssuer。如果集群里还没装:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace
然后照 cert-manager 文档 建一个 letsencrypt-prod ClusterIssuer。

数据库迁移

迁移在集群里跑。运行时镜像不带 drizzle-kit,也没有 Job 要照看。 取而代之,触动 schema 的发布之前,从本机跑:
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 里;大致长这样:
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 改好之后,每次部署是这样:
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:
./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/ 平行目录

故障排查

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

官方文档