跳转至

从「我的机器能跑」到容器化 — 生产准备、Docker 与 Compose

这一章讲什么: 应用做完只是原型——只跑在 localhost 上的东西还称不上 产品。这一章回答「上线到底意味着什么」:先把应用改造成生产的形状 (配置快速失败、健康检查、优雅关停、结构化日志),再把它打包成 容器镜像(在哪跑都一个样),最后在本地用 Docker Compose 把「前端 + API + 数据库」三件套完整彩排一遍。 读完你能解释:为什么 1.2 GB 的镜像是耻辱,以及 docker compose up 为什么是在给生产做预演。

1. 生产 mindset:九行差距表

开发的活法:tsx 热重载、Vite dev server、错误堆栈直接糊在脸上、 单用户、localhost。生产的活法:凌晨 3 点崩了得自己爬起来;用户看到 的是脱敏错误,堆栈进内部日志;一个连接变成 500 个;浏览器没 HTTPS 就罢工;出事了系统要自己恢复1。原书把差距列成一张九行对照表: 错误处理、环境变量、数据库连接、资源服务、进程管理、可观测性、 故障处理、HTTPS——每一行都是真用户到场后迟早要付的账1。

部署是一条流水线,而且每一段失败的姿势都不同:本地过的测试在 CI 里 因缺环境变量而挂;构建成功但镜像因 Node 版本错而崩;部署顺利但应用连 不上库(连接串错了)。没有一个是假想——这就是要自动化的理由2。

路线选择:FTP 传文件是老黄历;SSH+PM2 的传统 VPS 正被容器取代; Serverless 是另一种心智模型;学习路径的最优解:Docker 打包 + PaaS(Railway/Render/Fly.io)部署——Docker 是跨平台的通用技能, PaaS 是这个体量的项目最实际的路3。

2. 生产三件套:配置、健康检查、优雅关停

快速失败的环境配置

src/config.ts 在启动时就校验必需环境变量:DATABASE_URL 缺失 → 打印明确错误、立即退出。听着激进,想想反面:应用起来了、看着健康, 第一个用户登录时才炸出一段莫名其妙的数据库错误——带清晰消息的早死, 永远胜过看似健康的晚死。PORT 同时做类型和范围校验(1–65535), 并且用 Number() 不用 parseInt()——后者对 "3000abc" 会宽容地 吐出 3000,把畸形输入藏起来4。配套 .env.example 只记录变量名 不给真值;真 .env 永不进 git4。

健康检查:活着 ≠ 能用

  GET /healthz  → liveness:进程还能响应吗?永远回 200
  GET /readyz   → readiness:现在真能服务用户吗?
                  办法:prisma.$queryRaw`SELECT 1`
                  库挂了 → 503 → 平台把流量挪走

  命名来自 Kubernetes 社区,已成行业标准。

为什么要两个:进程活着但数据库连不上时,healthz 说一切正常, readyz 说此实例别接客。健康端点要注册在认证中间件之前—— 否则负载均衡器自己都验不了;响应保持最小化,不含任何秘密5。

优雅关停:部署不能吞掉正在提交的表单

平台部署新版时,给旧进程发 SIGTERM——操作系统的「请你关机」。 正确应答:server.close()(不再收新连接、等在途请求处理完)→ prisma.$disconnect()(关连接池)→ 干净退出。这就是零停机部署 的全部秘密:旧进程伺候完手头请求,新进程已经开始接客6。

三个工程细节。server.closeIdleConnections()(Node 18.2+)立即断开 闲着的 keep-alive 连接,免得它们拖住关停;shuttingDown 旗标防重复 信号跑两遍逻辑;10 秒强退超时是兜底——宁可强杀,不挂到天荒地老 (超时按你最长的请求调)6。

结构化日志:pino 与「敢不敢上公屏」测试

生产没人盯着终端看,日志是给解析、检索、聚合的机器吃的——所以输出 JSON 行({"method":"GET","path":"/api/users","status":200,"duration":45}) 而不是给人看的散文7。pino 用流式写,比 winston 快得多; redact: ['req.headers.authorization', 'req.headers.cookie', '*.password'] 把「别记敏感信息」从自律变成代码强制——就算哪行日志不小心带了令牌, 落盘前已被替换 [Redacted]。自检口诀:这行日志敢不敢出现在公共 仪表盘上?7

构建产物

后端:tsc 把 TS 编成 dist/(生产用 node dist/index.js 直接跑, 不要 tsx——它每文件现编译,有开销);module: "NodeNext" 正确建模 Node 的 ESM 解析(含导入路径要带 .js 后缀);前端项目则相反,用 "moduleResolution": "bundler"——前后端共用一个 tsconfig 是 经典踩坑8。前端 VITE_ 变量是构建时烘焙进产物的: staging 和生产的 API 地址不同?那就每个环境各构建一次(或改用运行时 拉 /config.json 的 workaround)8。

3. 主线例子:镜像减肥,1.2 GB → 150 MB

naive Dockerfile 六行就能跑,但 docker images 一看 1.2 GB—— 「开卡车去杂货店买两条面包」。里面全是不该上生产的东西:开发依赖、 TypeScript 源码、测试文件、完整的 Debian 底9。

多阶段版逐刀解剖:

  ① FROM node:24-alpine(两阶段都用)
     Alpine ≈ 5 MB 底 vs Debian ≈ 200 MB;镜像小=下载快、攻击面小
     ⚠ Alpine 用 musl 而非 glibc,原生模块(如 Prisma)可能抗议——
       出「query engine / binary target」错,先换 node:24-slim 再查别的
  ② 多阶段构建:builder 阶段装全套编译工具(tsc、dev deps),
     production 阶段从零开始,只 COPY 需要的(dist + prisma + 生产依赖)
     → 构建工具、源码、dev 依赖全部丢弃
  ③ npm ci --omit=dev && npm cache clean --force
     → 镜像里没有 vitest/typescript/tsx,也没有缓存残渣
  ④ COPY package*.json → RUN npm ci → COPY 源码(顺序即缓存策略)
     → 依赖没变只改代码时,Docker 整层跳过 npm install,构建从分钟变秒
  ⑤ USER node → 容器里不再以 root 跑;应用被攻破,攻击者没有 root
  ⑥ .dockerignore(node_modules、.env、dist、.git…)
     → 没它,Docker 会把整个目录(包括好几万文件的 node_modules)
       塞进构建上下文

成果:1.2 GB → 150–180 MB9。前端是另一条路:React 构建产物只是 静态文件,不需要 Node 运行时——builder 阶段构建,产物 COPY 进 nginx:stable-alpine(约 25 MB)。VITE_API_URL 靠 ARG/ENV 在构建时注入(烘焙特性决定的)10。

nginx.conf 里的三件宝:try_files $uri $uri/ /index.html——SPA 的 命门,用户直接访问 /dashboard 时回落到 index.html 交给 React Router; /assets/ 一年缓存(Vite 文件名带内容哈希,内容变哈希变); 安全头加 always(错误响应也带上)10。

4. 调试与日常命令

构建失败三板斧:--progress=plain --no-cache 看完整日志; --target builder 构建到中间阶段再 docker run -it sh 进去 「看看文件到底在哪」;本地成功 CI 失败九成是构建上下文—— .dockerignore 把文件排除后 COPY 看不见它(「本地能跑 CI 崩」 的经典原因);用临时目录 fresh git clone 复现 CI 环境11。 高频命令:build / run -p --env-file / ps / logs / exec -it sh / stop / images,Podman 逐字替换12。

5. Docker Compose:三服务一台戏

为什么学部署却花时间学一个「本地工具」?因为 Compose 让你在自家机器上 验证整个类生产编排——Dockerfile、环境变量、healthcheck、网络, 这里配的和生产用的是同一套;它是部署前的彩排13。

三服务:db(postgres:18-alpine + 命名卷——**没有卷,容器一停数据 就没了)、api、frontend。四个要点:

  • 服务名即主机名:API 的 DATABASE_URL 写 db:5432,不写 localhost——Compose 网络内按服务名互达;
  • depends_on: condition: service_healthy:不靠「容器启动了」 靠「真的健康了」。没这个,PostgreSQL 容器 running 好几秒后才真接客, API 抢跑会在首个查询崩掉——healthcheck 消灭这个启动竞态;
  • API 的 healthcheck 用 Node 内置 fetch 打自己的 /healthz, 让 frontend 等它真就绪;
  • 生产不暴露 5432(本地为方便才映射)13。

迁移在容器里怎么跑:启动脚本 start.sh = prisma migrate deploy + exec node dist/index.js。exec 不是洁癖——它让 Node 取代 shell 成为 PID 1,SIGTERM 才能直达应用,优雅关停代码才轮得到执行; 没有 exec,shell 占着 PID 1,部署信号永远到不了你的程序14。

6. 作者的判断与证据

  • 「学习路径 = Docker 打包 + PaaS 部署」是明确的教学选型建议3。
  • 「快速失败优于带病运行」是贯穿配置设计的原则4。
  • 「Alpine musl 问题先换 slim 再查别的」是经验之谈,优先级判断9。
  • 「Compose 是生产的预演而非目标形态」给工具定了正确的位置13。

7. 边界与局限

  • 零停机只在单实例轮换时成立:多实例滚动部署、数据库迁移与代码 版本兼容(expand/contract)等更硬的话题未展开;
  • 镜像安全(扫描漏洞、签名、最小权限)点到为止;
  • secrets 管理只说「平台托管、不进 git」,KMS/Vault 级方案未涉及;
  • Docker/Podman、Node 24 LTS 等版本口径为 2026 年中时点。

判断(我们的,不是书里的): 本章最高明的教法是故意从 naive Dockerfile 开始——让你亲手称出 1.2 GB,再逐刀讲每刀省了什么、 为什么。抽象清单(「用多阶段构建」「用 alpine」)背完即忘; 「卡车买面包」的称重视觉 + 逐刀归因,记得牢。 如果错,会错在: 「先跑通再优化」的顺序对学习最优,但对有安全 合规要求的团队是反模式——那些团队的第一版 Dockerfile 就该有 USER 非 root 和漏洞扫描;本书的渐进式是为教学设计的,不是为合规。

8. 可带走的

  1. 生产的差距是九行表:错误、配置、连接、静态资源、进程、可观测、自愈、HTTPS。
  2. 部署是流水线,每段失败姿势不同 → 自动化。
  3. 必需环境变量启动即验、缺了就死;Number 比 parseInt 诚实;.env 不进 git。
  4. healthz=活着,readyz=能接客(查数据库);注册在认证之前。
  5. SIGTERM → close(等完在途)→ 断库 → 退出;closeIdleConnections + 强退超时。
  6. 日志出 JSON;pino 的 redact 把「别记密码」变成强制。
  7. 后端 NodeNext / 前端 bundler,两本 tsconfig 别混。
  8. 镜像减肥五刀:alpine、多阶段、omit=dev、COPY 顺序(层缓存)、USER 非 root; .dockerignore 是「本地能跑 CI 崩」的常客。
  9. SPA 的 nginx 命门:try_files 回落 index.html;资产带哈希可长缓存。
  10. Compose:服务名当主机名、depends_on 等 healthy(消灭启动竞态); start.sh 用 exec 让 Node 当 PID 1,信号才达得到。

9. 原文地图

主题 原书章 原文位置
开发 vs 生产九行表 Chapter 13: Deploying Full-Stack Applications text/125-ch13-chapter-13-deploying-full-stack-applications.txt:115(搜「Error handling」;表跨 25–45 段)
部署流水线六段 What does "deployment" actually involve? text/126-fm-what-does-deployment-actually-involve.txt:7(搜「pipeline with multiple stages」)
路线选择 The modern deployment landscape text/127-fm-the-modern-deployment-landscape.txt:9(搜「PaaS (Platform as a Service)」) · text/127-fm-the-modern-deployment-landscape.txt:55(搜「Docker as the packaging」)
快速失败配置 Environment configuration text/128-fm-environment-configuration.txt:52(搜「crashes immediately」) · text/128-fm-environment-configuration.txt:76(搜「parseInt()」)
healthz/readyz Health check endpoints text/129-fm-health-check-endpoints.txt:51(搜「liveness」) · text/129-fm-health-check-endpoints.txt:115(搜「// Health checks first, before any auth 」)
优雅关停 Graceful shutdown text/130-fm-graceful-shutdown.txt:79(搜「This is the key to zero-downtime deploym」) · text/130-fm-graceful-shutdown.txt:61(搜「server.closeIdleConnections();」)
pino 与 redact Production logging text/131-fm-production-logging.txt:39(搜「public dashboard」) · text/131-fm-production-logging.txt:90(搜「Redacted」)
两本 tsconfig Building for production text/132-fm-building-for-production.txt:55(搜「moduleResolution: "bundler"」)
1.2 GB 与逐刀解剖 Dockerizing the Express API text/134-fm-dockerizing-the-express-api.txt:44(搜「, you will see it is around 1.2 GB.」) · text/134-fm-dockerizing-the-express-api.txt:72(搜「Replace the Dockerfile with this multi-s」) · text/134-fm-dockerizing-the-express-api.txt:327(搜「layer caching」)
Alpine musl 警告 Dockerizing the Express API text/134-fm-dockerizing-the-express-api.txt:56(搜「musl」)
nginx 三件套 Dockerizing the React frontend text/135-fm-dockerizing-the-react-frontend.txt:134(搜「try_files $uri $uri/ /index.html;」) · text/135-fm-dockerizing-the-react-frontend.txt:235(搜「content hashes」)
构建调试三板斧 Debugging failed builds text/136-fm-debugging-failed-builds.txt:17(搜「--progress=plain」) · text/136-fm-debugging-failed-builds.txt:114(搜「git clone」)
Compose 即预演 Running the stack text/137-fm-essential-container-commands.txt:65(搜「rehearsal」)
服务名即主机名与健康检查 Running the stack text/138-fm-running-the-stack.txt:33(搜「db」);healthcheck 段见 137 文件(text/137-fm-essential-container-commands.txt:87 搜「pg_isready」)
start.sh 与 exec PID 1 Running Prisma migrations in Docker text/139-fm-running-prisma-migrations-in-docker.txt:22(搜「exec node dist/index.js」) · text/139-fm-running-prisma-migrations-in-docker.txt:28(搜「command replaces the shell process with 」)


  1. 出处:「Chapter 13: Deploying Full-Stack Applications」第 25–55 段(text/125-ch13-chapter-13-deploying-full-stack-applications.txt:95,搜「completely different world」)。 ↩↩

  2. 出处:「What does "deployment" actually involve?」第 5–31 段(text/126-fm-what-does-deployment-actually-involve.txt:25,搜「fail differently」;:31,搜「automate」)。 ↩

  3. 出处:「The modern deployment landscape」第 37–59 段(text/127-fm-the-modern-deployment-landscape.txt:55,搜「Docker as the packaging」)。 ↩↩

  4. 出处:「Environment configuration」第 11–47 段(text/128-fm-environment-configuration.txt:52,搜「crashes immediately」;:33,搜「parseInt(\"3000abc\")」;:45,搜「Never commit」)。 ↩↩↩

  5. 出处:「Health check endpoints」第 13–49 段(text/129-fm-health-check-endpoints.txt:51,搜「liveness」;:27,搜「before any auth」;:43,搜「Kubernetes community」)。 ↩

  6. 出处:「Graceful shutdown」第 9–45 段(text/130-fm-graceful-shutdown.txt:79,搜「This is the key to zero-downtime deploym」;:39,搜「closeIdleConnections」;:45,搜「safety net」)。 ↩↩

  7. 出处:「Production logging」第 11–39 段(text/131-fm-production-logging.txt:39,搜「public dashboard」;:35,搜「Redacted」)。 ↩↩

  8. 出处:「Building for production」第 21–63 段(text/132-fm-building-for-production.txt:55,搜「moduleResolution: "bundler"」;:47,搜「baked into the build」)。 ↩↩

  9. 出处:「Dockerizing the Express API」第 13–51 段与 93–105 段(text/134-fm-dockerizing-the-express-api.txt:44,搜「, you will see it is around 1.2 GB.」;:31,搜「musl」;:45,搜「multi-stage」;:99,搜「layer caching」)。 ↩↩↩

  10. 出处:「Dockerizing the React frontend」第 27–65 段(text/135-fm-dockerizing-the-react-frontend.txt:93,搜「ARG」;:45,搜「try_files」;:61,搜「content hashes」)。 ↩↩

  11. 出处:「Debugging failed builds」第 7–41 段(text/136-fm-debugging-failed-builds.txt:17,搜「--progress=plain」;:37,搜「clean checkout」)。 ↩

  12. 出处:「Essential container commands」第 3–29 段(text/137-fm-essential-container-commands.txt:8,搜「docker build -t myapp-api .」)。 ↩

  13. 出处:「Essential container commands」第 31–55 段(编排动机与 Compose 定位,text/137-fm-essential-container-commands.txt:65,搜「rehearsal」)。 ↩↩↩

  14. 出处:「Running Prisma migrations in Docker」第 9–21 段(text/139-fm-running-prisma-migrations-in-docker.txt:22,搜「exec node dist/index.js」;:17,搜「SIGTERM」)。 ↩