上线之后 — CI/CD、平台部署、生产运维与伸缩的边界
上线之后 — CI/CD、平台部署、生产运维与伸缩的边界
这一章讲什么: 「部署成功」不是终点,是运维的起点。 流水线替你守质量门(类型检查→测试→构建),平台替你管机器, 但连接池、备份、迁移顺序、安全中间件的次序、账单——这些没人替你操心。 读完你会有一张上线清单,以及一张「什么时候才需要 Kubernetes」的判定卡。
1. CI/CD:依赖链与质量门
GitHub Actions 的结构三层:workflow(什么时候跑)→ job(并行/串行的
任务块)→ step(命令或社区 action)。on: push/pull_request 定义触发器;
strategy.matrix 让同一段步骤对 api 和 frontend 各跑一遍(并行两份)1。
四个 job 排成依赖链,这是本章第一个要学的形状1:
lint-and-typecheck(matrix: api+frontend)
│ needs
▼
┌─────────────┬──────────────┐
test-api test-frontend
(带真 Postgres (npm run build
容器做服务!) 验证产物构建)
└──────┬───────┘
▼ needs [test-api, test-api]
build-docker(两个镜像,gha 缓存
命中时 5 分钟 → 30 秒)要点三个。测试 job 用真数据库:workflow 的 services: 段起一个
PostgreSQL 容器,health check 等它真就绪——「mock 库能过的测试,
真查询可能生产才炸」1。缓存:cache: 'npm' 省 60–80% 安装时间;
Docker 层走 cache-from: type=gha。分支保护是把 CI 变成门的最后
一颗螺丝:main 设为「status checks 全过才许合并」——从此无论谁,
同样的检查跑在每一个改动上2。
2. 平台:推代码即上线
选型一张表(Railway 最易上手/Render 价格可预测/Fly.io 全球低延迟/
VPS 全自控);三大云(ECS/Cloud Run 们)被刻意排除——学习阶段
别淹在云厂商的控制台里,概念以后平移3。Railway 路径四步:
加一个 PostgreSQL(托管、自动给连接串)→ 从 GitHub 仓库加 API 服务
(检测到 Dockerfile 自动构建)→ 环境变量进平台面板 → 加前端服务
(VITE_API_URL 是构建参数;部署后改它不生效——变量烘焙在
构建产物里,改地址必须重建前端)。最后回头把 API 的 CORS_ORIGIN
改成前端的线上地址4。
冒烟测试四连(上线后一分钟内做完,别等用户替你测):/healthz 200、
/readyz 200、打开前端看控制台、跑一条真实数据流(登录→建一条记录)、
翻平台日志找启动/迁移/CORS 错误4。VPS 路线的现代化简版:
Caddy 当反代——自动申请并续期 Let's Encrypt 证书,告别
「nginx + certbot + cron」的三件套时代5。
3. 生产数据库:三条会要你命的坑
只准 prisma migrate deploy——生产禁用 migrate dev、禁用
db push;deploy 只补跑未应用的迁移,不建新的、不删表、不问确认。
迁移失败会被 _prisma_migrations 表标记为 failed,只能人工解决;
所以部署挂了先看日志,别反复重发6。
连接池会先于你的代码倒下。三实例 × 各自的池,很容易顶到
PostgreSQL 默认 100 连接上限;容器里池大小还可能按宿主 CPU 数算,
虚高。连接串加 ?connection_limit=5 显式收;大了上 PgBouncer。
坑中之坑:迁移不要走连接池——advisory lock 不被池支持;
平台给了 pooled/direct 两条连接串时,应用用 pooled、迁移用 direct。
池配错的典型死状:「too many connections」,高负载下发作,
长得就像数据库 itself 挂了7。
备份要试恢复:pg_dump 定时 + 异地存放只是上半场;「从没恢复过
的备份等于没有备份」——定期真恢复一次8。生产 seed 的规矩:
必须存在的数据(初始管理员、默认分类)写进迁移 SQL——版本化、
部署时自动应用;prisma db seed 是开发工具,生产禁用8。
4. 生产安全:次序就是安全
几条铁律:密钥永不为每个环境复用(开发/预发/生产各一套),离职即轮换; 栈跟踪和内部路径永不进 API 响应9。
npm audit 进 CI:学习项目可以 --audit-level=high +
continue-on-error: true(不被深层传递依赖的低风险公告卡死部署),
但「没人看的警告等于没有检查」——生产团队要么提到 critical 并硬失败,
要么挂进定人负责的维护流程10。
中间件次序(一个真实的生产 bug 发生器)11:
app.set('trust proxy', 1) ← 最前:平台反代在转发真实 IP,
不设则限流把全站用户当一个 IP 封
helmet() ← 安全头
cors({ origin, credentials }) ← 注意尾部斜杠!origin 不带 /,
一个字符错配 = 全部跨域静默失败
healthRoutes ← 健康检查在限流之前,平台才能自由轮询
/api/auth 的严限流
/api 的宽限流 ← 认证端点会被两个限流叠加;
想避免就把 auth 路由挂载出宽限流多实例部署还有一坑:express-rate-limit 默认内存计数,两实例=每人
两个配额;多实例要接 Redis store11。CORS 是部署后最高频的故障:
前端能开、API 静默失败 → 看控制台,逐字符核对 origin11。
5. 伸缩的边界:Compose 之后是什么,什么时候才轮到 K8s
单机 Compose 的天花板:机器一崩全灭;流量翻倍没法拆两台。编排器 解决的问题就是跨机调度。知识地图(都是「知道存在」级)12:
| 概念 | 一句话 | Compose 对应 |
|---|---|---|
| Pod | 最小部署单元(≈ 被托管的 docker run) | 一个 service 容器 |
| Deployment | 「N 个副本永远活着」;崩了自动补;发新版滚动替换 | services: 本体 |
| Service | Pod 来来去去,网络地址不变 | ports: |
| Ingress | 一组机器(集群)对外流量的反代 | 无 |
Deployment 清单里能看到前面所有伏笔兑现:readinessProbe 打
/readyz(临时库故障 = 摘流量,不必重启)、livenessProbe 打
/healthz(进程卡死 = 重启);resources requests/limits 防一个
疯 Pod 拖垮整节点;密钥走 secretKeyRef(含明文 Secret 的清单
别进 git)12。迁移在 K8s 里是单独的 Job 或流水线一步——
三个 Pod 同时各跑一遍迁移,就是事故13。
判定卡:Compose 够用就不要 K8s。 它值得的场景只有四种: 按流量自动扩、多区域、微服务间复杂通信、强 IaC 合规要求;否则 PaaS 更简单也常常更便宜。要练手用 minikube/kind/Docker Desktop 的内置集群;真上生产用托管 K8s(GKE/EKS/AKS),控制面别自己养13。
IaC(Terraform/OpenTofu/Pulumi):点按钮建出来的环境没法复现给 staging、没法交给队友、没法灾后重建——把基础设施写成代码, 走 PR 评审。TypeScript 开发者看 Pulumi:基础设施用你已经在用的语言写14。 Aspire 补的是另一层缺口:多服务开发的本地体验统——一个仪表板看 全部服务的日志、健康、端口,不取代 Docker/K8s14。
6. 上线之后:更新策略与账单
更新即推 main:CI 过→平台滚动部署(新版本通过健康检查后切流量, 旧版本确认退场才停)。三种进阶策略存个名字:blue/green(双环境瞬时 切换、秒级回滚、双倍成本)、canary(先放 5% 流量观察)、feature flags(部署与发布解耦:代码上线但不激活)15。
迁移与更新的次序铁律:迁移先跑,代码后发(所以启动脚本先
migrate deploy 再起服务:迁移失败则旧版继续接客)。回滚的真相:
代码能回滚,加过列的表回不去——回滚代码不等于回退 schema。
解法:expand-contract——第一步迁移只加新列(expand),第二步发
用新列的代码,第三步再删旧列(contract);新老代码在过渡期共用
同一 schema,回滚才安全。绝不把破坏性 schema 变更和业务代码变更
塞进同一次部署15。
成本章给了真实数字感的对照(2026 中时点,会过期):轻量 API 空闲时 80–150 MB 内存、半核 vCPU 就够;前端静态托管≈免费;PostgreSQL 才是 重头(256 MB–1 GB 足矣)。月费量级:Railway $10–20、Render ~$14、 Fly.io $2–6 起、Hetzner VPS €4–8(代价是全都自己管)。学习期的省钱 四招:不用就暂停、容器设资源上限、盯用量面板、能用免费层就别付费16。
7. 作者的判断与证据
- 「真库测试 > mock 测试(在 CI 里)」是明确的质量判断1。
- 「Compose 够用就不上 K8s」给了四条具体触发条件,不是模糊劝退13。
- 「警告没人读等于没有检查」「没恢复过的备份不算备份」是运维谚语级 判断,也是全书最该带走的句子108。
- 「前向迁移优于回滚」+ expand-contract 是行业通行最佳实践,与 Prisma 官方建议一致。
8. 边界与局限
- Kubernetes 全部停留在概念地图( knowingly——本书定位是 「知道存在、何时用」);
- 多实例会话一致性、分布式限流/队列只提了 store 选项,没展开;
- 合规与审计(SOC2、数据驻留)只在托管认证服务处侧面提到;
- 平台价格、免费层、GitHub Actions 计费都是强时点信息(2026 年中), 引用前必查当下文档。
判断(我们的,不是书里的): 本章最健康的姿态是克制: K8s/IaC/Aspire 三章只给「认知地图 + 触发条件」,没有为凑深度而 塞操作手册——对一个 20 章的教学项目,「知道自己还不知道什么」 比「照抄一份 YAML」有价值得多。「回滚的真相」(代码回得去、 schema 回不去)则是全章含金量最高的一段:它把「部署顺序」从 操作习惯升格为设计约束。 如果错,会错在: 「PaaS 足够」的口径对小团队成立,但 2026 年 出现了「PaaS 定价飙升 + 云原生工具平民化」的反向趋势(如 Coolify/Dokku 这类自托管 PaaS),「何时离开 PaaS」的边界可能 比书中画的更模糊。
9. 可带走的
- CI 依赖链:类型检查 →(真库)测试 → Docker 构建;分支保护让 CI 成为门。
- 测试 job 用 services: 起真 PostgreSQL——mock 库过的测试可能生产才炸。
- 平台部署四步;
VITE_API_URL是构建参数,改了必须重建前端。 - 上线冒烟四连:healthz/readyz/控制台/真实数据流。
- 生产迁移只准
migrate deploy;失败看日志,别反复重发。 - 连接池:实例数×池大小会顶爆默认 100;迁移走 direct 不走 pooled。
- 备份要真恢复一次;必须数据进迁移 SQL,生产禁用 db seed。
- 中间件次序:trust proxy → helmet → cors → 健康检查 → 认证限流 → API 限流。
- 回滚的真相:代码回得去、schema 回不去;破坏性变更与代码变更分两次部署; expand-contract 三步走。
- Compose 够用不上 K8s;触发条件是自动扩/多区域/微服务/强合规。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| workflow/job/step 与 matrix | Building our CI pipeline | text/141-fm-building-our-ci-pipeline.txt:29(搜「matrix:」) |
| services 真库测试 | Building our CI pipeline | text/141-fm-building-our-ci-pipeline.txt:193(搜「real PostgreSQL database」) |
| 分支保护 | Branch protection | text/142-fm-branch-protection.txt:9(搜「Require status checks」) |
| 平台选型与排除三大云 | Choosing a platform | text/143-fm-choosing-a-platform.txt:83(搜「deliberately absent」) |
| Railway 四步与重建前端 | Deploying to Railway | text/144-fm-deploying-to-railway.txt:49(搜「trigger a rebuild」) |
| 冒烟测试 | Deploying to Railway | text/144-fm-deploying-to-railway.txt:61(搜「smoke test」) |
| Caddy 自动证书 | Deploying with Docker to a VPS (advanced) | text/146-fm-deploying-with-docker-to-a-vps-advanced.txt:19(搜「Caddy」) |
| 生产迁移铁律 | Running migrations in production | text/147-fm-running-migrations-in-production.txt:7(搜「Never」) |
| 连接池与 PgBouncer | Connection pooling | text/148-fm-connection-pooling.txt:29(搜「advisory locks」) |
| 备份要试恢复 | Backups | text/149-fm-backups.txt:17(搜「never restored」) |
| 生产 seed 规矩 | Seeding production data | text/150-fm-seeding-production-data.txt:23(搜「Never use」) |
| trust proxy 与限流次序 | Rate limiting | text/153-fm-rate-limiting.txt:15(搜「trust proxy」) · text/155-fm-putting-security-middleware-together.txt:7(搜「The order in which you register matters.」) |
| CORS 尾部斜杠 | CORS in production | text/154-fm-cors-in-production.txt:22(搜「trailing slashes」) |
| 没人读的警告 | Dependency security | text/156-fm-dependency-security.txt:42(搜「nobody reads」) |
| K8s 四概念 | Kubernetes core concepts | text/159-fm-kubernetes-core-concepts.txt:23(搜「smallest deployable unit.」) · readiness/liveness 见 text/160-fm-from-docker-compose-to-kubernetes.txt:29(搜「readinessProbe」) |
| 迁移是 Job | From Docker Compose to Kubernetes | text/160-fm-from-docker-compose-to-kubernetes.txt:184(搜「separate Job」) |
| Compose 够用的判定 | When to use Kubernetes | text/161-fm-when-to-use-kubernetes.txt:5(搜「not yet」) |
| IaC 三工具 | Infrastructure as Code (IaC) | text/162-fm-infrastructure-as-code-iac.txt:35(搜「Terraform」) |
| 滚动/蓝绿/金丝雀/feature flags | Updating your deployed application | text/165-fm-updating-your-deployed-application.txt:23(搜「rolling」) · text/165-fm-updating-your-deployed-application.txt:109(搜「expand-contract」) |
| 回滚的真相 | Updating your deployed application | text/165-fm-updating-your-deployed-application.txt:97(搜「does not remove the column」) |
| 成本量级 | Cost awareness | text/166-fm-cost-awareness.txt:15(搜「80-150 MB」) |
Footnotes
-
出处:「Building our CI pipeline」第 3–51 段(
text/141-fm-building-our-ci-pipeline.txt:29,搜「matrix:」;:35,搜「real PostgreSQL database」)。 ↩ ↩2 ↩3 ↩4 -
出处:「Branch protection」第 3–15 段(
text/142-fm-branch-protection.txt:9,搜「Require status checks」)。 ↩ -
出处:「Choosing a platform」第 21–29 段(
text/143-fm-choosing-a-platform.txt:83,搜「deliberately absent」)。 ↩ -
出处:「Deploying to Railway」第 21–37 段(
text/144-fm-deploying-to-railway.txt:49,搜「trigger a rebuild」;:31,搜「smoke test」)。 ↩ ↩2 -
出处:「Deploying with Docker to a VPS (advanced)」第 9–25 段(
text/146-fm-deploying-with-docker-to-a-vps-advanced.txt:19,搜「Caddy」)。 ↩ -
出处:「Running migrations in production」第 3–19 段(
text/147-fm-running-migrations-in-production.txt:7,搜「Never」;:17,搜「_prisma_migrations」)。 ↩ -
出处:「Connection pooling」第 3–31 段(
text/148-fm-connection-pooling.txt:29,搜「advisory locks」;:25,搜「too many connections」)。 ↩ -
出处:「Backups」第 5–9 段与「Seeding production data」第 13–15 段(
text/149-fm-backups.txt:17,搜「never restored」;text/150-fm-seeding-production-data.txt:23,搜「Never use」)。 ↩ ↩2 ↩3 -
出处:「Environment and secrets」第 3–13 段与「Additional security considerations」第 25–31 段(
text/151-fm-environment-and-secrets.txt:13,搜「Never commit」;text/157-fm-additional-security-considerations.txt:55,搜「Never expose stack traces」)。 ↩ -
出处:「Dependency security」第 3–25 段(
text/156-fm-dependency-security.txt:42,搜「nobody reads」)。 ↩ ↩2 -
出处:「Rate limiting」第 5–19 段、「Putting security middleware together」第 3–31 段、「CORS in production」第 9–17 段(
text/153-fm-rate-limiting.txt:15,搜「trust proxy」;text/155-fm-putting-security-middleware-together.txt:7,搜「The order in which you register matters.」;text/154-fm-cors-in-production.txt:22,搜「trailing slashes」)。 ↩ ↩2 ↩3 -
出处:「Kubernetes core concepts」第 3–21 段与「From Docker Compose to Kubernetes」第 25–45 段(
text/159-fm-kubernetes-core-concepts.txt:23,搜「smallest deployable unit.」;text/160-fm-from-docker-compose-to-kubernetes.txt:29,搜「readinessProbe」)。 ↩ ↩2 -
出处:「From Docker Compose to Kubernetes」第 47–51 段与「When to use Kubernetes」第 3–15 段(
text/160-fm-from-docker-compose-to-kubernetes.txt:184,搜「separate Job」;text/161-fm-when-to-use-kubernetes.txt:5,搜「not yet」)。 ↩ ↩2 ↩3 -
出处:「Infrastructure as Code (IaC)」第 3–45 段与「A glimpse of Microsoft Aspire」第 3–15 段(
text/162-fm-infrastructure-as-code-iac.txt:35,搜「Terraform」;text/163-fm-a-glimpse-of-microsoft-aspire.txt:31,搜「The Aspire dashboard gives you a unified」)。 ↩ ↩2 -
出处:「Updating your deployed application」第 3–55 段(
text/165-fm-updating-your-deployed-application.txt:23,搜「rolling」;:33,搜「does not remove the column」;:41,搜「expand-contract」)。 ↩ ↩2 -
出处:「Cost awareness」第 3–45 段(
text/166-fm-cost-awareness.txt:15,搜「80-150 MB」)。 ↩