讲解
编译型应用有个两难:构建需要编译器、头文件、依赖缓存(几百 MB 甚至上 GB),运行时只需要一个二进制。如果都在一个镜像里,最终镜像背着整个构建工具链——又大又不安全(攻击面大)。多阶段构建(multi-stage build)在一个 Dockerfile 里写多个 FROM,每个 FROM 开启一个阶段,后面阶段用 COPY --from=阶段名 从前面的阶段精准取走产物,前面的阶段整体不进最终镜像。
最典型的模式是「构建者 + 运行者」:第一阶段用 node:22(或 golang、maven)完整镜像装依赖、编译;第二阶段用 alpine/distroless 这种最小镜像,只 COPY 编译产物。最终镜像里连构建工具的影子都没有——Node 项目常见从 1GB+ 降到 100MB 量级,Go 项目可以小到几 MB(静态编译 + scratch)。
两个使用技巧:阶段可以命名(FROM node:22-alpine AS build),引用名字比引用序号(--from=0)可读性好;docker build --target 阶段名 可以只构建到某个阶段——开发时用带工具链的阶段当调试镜像,CI 构建到最终阶段,一份 Dockerfile 两种产出。多阶段不只为编译型语言服务:前端项目(node 构建静态文件 + nginx 托管)同样是它的标准用法。
示例
构建上下文:一个「需要构建步骤」的 Node 小应用(构建步骤用 node --check 做语法校验模拟):
mkdir -p msctx && cat > msctx/server.js <<'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
res.end('multi-stage build works\n');
});
server.listen(3000, () => console.log('listening on 3000'));
EOF
两阶段:build 阶段校验代码,run 阶段只带走产物——最终镜像不含任何「构建动作」的痕迹:
# context: msctx
FROM node:22-alpine AS build
WORKDIR /src
COPY server.js .
RUN node --check server.js && echo "构建校验通过"
FROM node:22-alpine
WORKDIR /app
COPY --from=build /src/server.js .
EXPOSE 3000
CMD ["node", "server.js"]
构建并对比——history 里能看到最终镜像只有来自第二个阶段的层:
cd msctx && docker build -t ohmydocs-verify-multi:1.0 . && cd ..
docker history ohmydocs-verify-multi:1.0 --format '{{.CreatedBy}}' | head -5
运行验证产物可用:
docker run -d --name ohmydocs-verify-msapp -p 18084:3000 ohmydocs-verify-multi:1.0
sleep 2 && curl -sf http://localhost:18084
docker rm -f ohmydocs-verify-msapp
常见坑
- COPY --from 路径写错:源路径是「构建阶段里的绝对路径」,不是宿主机路径;阶段里 WORKDIR 是什么,路径就从那里算。
- 最终阶段还装依赖:node_modules 在构建阶段装、生产依赖要在最终阶段重新装(npm ci --omit=dev),别把 node 全量镜像当运行时。
- --target 调试用错阶段:调试镜像和发布镜像行为不同,发布流水线里固定 target,别让「本地能跑」误导。
- 阶段间隐式依赖:改 Dockerfile 时注意缓存失效链条(下一章),构建阶段的代码 COPY 位置影响复用率。
小结
多个 FROM 分阶段,COPY --from 精准取产物,最终镜像不含工具链;命名阶段 + --target 支持调试/发布双模式。下一章讲构建提速:缓存与 .dockerignore。