多阶段 Docker
B8 这一段把 agent runner 从「脚本」推向「真服务」。
阶段: B8 · 真 API + 流式 + Docker(Day 71-80) 标签: #docker #multi-stage #distroless #layer-cache
今日导引(由浅入深)
B8 这一段把 agent runner 从「脚本」推向「真服务」。 前几天打通了 HTTP 边界(Day 71)、SSE(Day 72)、模型流式(Day 73)、typed 错误(Day 74)、eval 接 API(Day 75)、双模型横评(Day 76)。
代码层面跑通之后,要让别人 / CI / 云能复现地跑起来,就得封装成镜像——这是 B8→B9(部署)的桥。 今天学多阶段构建:把「编译时需要的一大堆 devDeps」和「运行时只需要的产物」彻底分层,再叠 distroless 基镜与 layer 缓存顺序,把镜像压小、攻击面缩小。
今天的「最小可判定产出」是用 Dockerfile.mcp 跑一次 docker build 后 docker images 读出首版体积数字——这是 Day 78 瘦身的基线锚点。
1. 机理精读
多阶段构建(multi-stage build)的核心是「构建产物 ≠ 运行依赖」。
一个 Node 服务,编译 / 装包阶段要 pnpm、tsc/tsx、全套 devDependencies;
但真正 docker run 起来时,这些大半都是死重。
多阶段把 Dockerfile 切成几个 FROM ... AS <stage>:
在 deps / builder 阶段装全套、编译;
在 runtime 阶段只 COPY --from=deps 把需要的产物搬过来。
最终镜像只含 runtime 阶段的内容,devDeps 的体积和攻击面留在了不进入最终镜像的中间层——它们只是「构建脚手架」,build 完即弃。
distroless 基础镜像把攻击面再削一层。 distroless 镜像(Google 维护)不含 shell、包管理器、coreutils——只有语言运行时和你的应用。 好处有二:
- 体积小——少了一整套 OS 用户态工具。
- 安全——攻击者拿到 RCE 也没有
sh/apt/curl可用,横向移动被掐死。
代价是调试难(进不去 shell),但生产 runtime 本就不该让人 exec 进去戳。
layer 缓存顺序决定增量构建快慢。 Docker 按层缓存,某层的输入没变就复用缓存。 黄金法则:变化频率低的放前面,高的放后面。
所以「先 COPY lockfile 装依赖、后 COPY 源码」——
依赖清单几周才动一次,依赖安装层能长期命中缓存;
源码天天改,放最后,改源码只重建最后几层。
反过来先 copy 源码再装依赖,每改一行代码都得重装全部依赖,CI 慢到崩——这是新手最常见的 Dockerfile 反模式。
与相邻概念的边界。
多阶段解决「最终镜像别装编译期垃圾」;
distroless 解决「runtime 基镜别带攻击面」;
BuildKit 缓存挂载(--mount=type=cache)解决「跨构建复用包管理器缓存」。
三者正交、可叠加,明天(Day 78)的 <300MB 目标正是三者合力 + Next standalone + 严格 .dockerignore。
2. 推导 / 手算 / 代码走读
代码走读 Dockerfile.mcp(仓库已有,跑 MCP server,无需 API key):
FROM node:20-slim AS base: 基镜选node:20-slim(非 distroless 但已是 slim 变体),设PNPM_HOME=/pnpm与PATH=$PNPM_HOME:$PATH,RUN corepack enable启用 pnpm,WORKDIR /app。这是后续两阶段的共同底座。FROM base AS deps: 先COPY package.json pnpm-lock.yaml ./,再RUN pnpm install --frozen-lockfile—— 只 copy lockfile 再装依赖,正是 layer 缓存的黄金顺序;--frozen-lockfile保证可复现(lockfile 不一致直接失败,不偷偷改版本)。FROM base AS runtime:COPY --from=deps /app/node_modules ./node_modules把上一阶段装好的依赖搬过来(关键的「多阶段」动作), 再 copypackage.json/pnpm-lock.yaml/tsconfig.json、src、scripts。- 运行配置:
ENV PORT=8765、EXPOSE 8765、CMD ["pnpm", "mcp:serve"]。 注释明说 "tsx (devDependency) executes the TS entrypoint directly; the MCP server is stateless HTTP."—— 当前用 tsx 直跑 TS,无单独编译阶段(明天瘦身可换 standalone 输出)。 - 顶部注释给出确切命令:
build:
docker build -f Dockerfile.mcp -t momoweb3-mcp .run:docker run -p 8765:8765 -e MCP_AUTH_SECRET=dev-secret momoweb3-mcp(省略MCP_AUTH_SECRET即关掉 OAuth gate,本地 dev 用——见 B7 的授权层)。
走读结论:当前
Dockerfile.mcp是 base→deps→runtime 三段, 但 runtime 仍从deps搬整套node_modules(含 devDeps),尚未做 prod-only 裁剪—— 这正是 Day 78 瘦身的优化目标,今天先量首版体积、确立基线。
3. 今日实战
- 确认前置件就位:
Dockerfile.mcp+.dockerignore(✅ 已构建,build 是手动 docker step)。 - 本机跑:
docker build -f Dockerfile.mcp -t momoweb3-mcp .(首次会装全套依赖,注意观察各层缓存命中情况)。 docker images读出momoweb3-mcp的首版体积(MB),记入 worklog 作瘦身前基线。- 改一行源码后重 build,验证只重建最后几层(layer 缓存命中验证)。
- 可选
docker run -p 8765:8765 momoweb3-mcp起容器验证pnpm mcp:serve能拉起(无MCP_AUTH_SECRET即跳过 OAuth gate)。
4. 今日实测 / 产出
Dockerfile.mcp与.dockerignore已构建(artifact 在仓库)。- 首版镜像体积数字(MB)为 外部动作 / 手动 docker step——需在本机跑
docker build后读数。 - CI 未自动构建镜像(诚实标注:仓库侧只有 Dockerfile,云 / CI 自动 build 链路是 B9 待接)。
5. 常见误区 / 陷阱
- 缓存顺序写反:
先
COPY . .再装依赖,改一行源码就触发全量重装,CI 时间炸裂。务必先 copy lockfile 装依赖、后 copy 源码。 - 把 devDeps 漏进 runtime 层:
本仓当前
Dockerfile.mcp就从 deps 搬了整套 node_modules(含 devDeps)——首版体积偏大的主因,留给 Day 78 用--prod裁。 - 直接用
node:latest全量基镜: 数 GB、攻击面大。最少用 slim,能 distroless 更好。 - 忘了
.dockerignore: 不排除node_modules/.git/docs会把构建上下文撑爆,并可能把本地 node_modules 误打进镜像。
6. 学习资源(每条带 YYYY-MM)
- Docker 官方文档 — Multi-stage builds(Docker, 2025-11 持续更新版)。
- Distroless 项目(GoogleContainerTools/distroless, 2025-10)——
distroless/nodejs运行时镜像与最小攻击面理念。 - Docker BuildKit — Cache mounts(
--mount=type=cache)文档(Docker, 2025-09)。 - Next.js — Output File Tracing /
output: 'standalone'(Vercel, 2026-01)——明天瘦身的关键。 - 本仓 artifact:
Dockerfile.mcp、.dockerignore(2026-06,AICAP-180 B8)。
SOTA检查 (2026-06 更新)
- 多阶段 + distroless 仍是镜像瘦身 SOTA。Node 场景推荐 standalone 输出 +
distroless/nodejs运行时。 - BuildKit 缓存挂载为当前推荐做法(跨构建复用 pnpm / npm 缓存)。
- 过时黑名单:直接
node:latest全量基镜(数 GB、攻击面大);缓存顺序写反;devDeps 进 runtime 层。 - 下次复查点:Day 78 量化瘦身效果(目标 <300MB);BuildKit / distroless 版本执行当周核对。
衔接
- 昨天:Day 76 — 流式接 Qwen3 对比(双模型横评,代码层跑通选型矩阵)
- 今天:把跑通的服务用多阶段 Dockerfile 封装,量首版镜像体积——可复现部署的第一步
- 明天:Day 78 — 镜像瘦身 <300MB(叠 prod-only / standalone / .dockerignore,把首版体积压下来)