本文摘要:多阶段构建上线后镜像大出三百兆,编译器明明已被挡在外面。逐层记账定位到粗粒度
COPY --from,源码与依赖被整体拷进运行层。该方案仅适用于产物目录清晰的项目,体积与缓存指标须分开对齐。
一、问题与结论
先把基线钉死:这次比较的对象是同一条分支上一版单阶段镜像 app:1.4,新构建的多阶段镜像 app:1.5 在 docker images 里比它大约 300MB。换一个基线(比如和 registry 上另一个 tag 比),结论会完全不同,排查前必须写清"和谁比"。
结论先给:多阶段构建只保证最终阶段不再执行 RUN apt-get、RUN npm ci,不保证镜像变小——体积由最终阶段自己的 COPY 内容和所选基础镜像决定。逐层查下来,这 300MB 全部落在一条 COPY --from=build /src /app 上:源码、含 devDependencies 的全量 node_modules、构建缓存都被压进运行阶段的一个新层;runtime 用的又与 builder 同款完整版 bookworm,多阶段实际只挡住了"在最终阶段装工具"这一件事。
二、排查与选择依据
固定顺序:定基线 → docker history 找最大层 → 对齐体积口径 → 最后才清理缓存。跳过前两步直接 prune,是在解决另一个问题。
第 1 步,逐层记账,看体量最大那层的 CreatedBy:
docker history app:1.5 --format 'table {{.Size}}\t{{.CreatedBy}}'
--format 占位符名以本机 docker history --help 输出为准(未验证项,发布前需核验)。第 2 步,对齐口径:
docker image inspect app:1.5 --format '{{.Size}}' # 各层之和,共享基础层重复计入
docker system df # 磁盘占用,共享层只算一次
docker buildx du # builder 缓存,不是镜像大小
docker images -f dangling=true # legacy builder 可能残留的中间镜像
docker buildx du 显示几个 GB 就去删缓存,镜像一点不会变小——这是最常见的误判。
替代方案与取舍
| 做法 | 选择条件 | 代价与边界 |
|---|---|---|
只收窄 COPY --from 粒度,产物与运行时依赖分两路拷贝 | 产物目录清晰(如 dist/)、依赖可单独成层 | 改动最小、收益直接;依赖路径写错会在运行期报 MODULE_NOT_FOUND |
runtime 阶段降级到 -slim/-alpine 基础镜像 | 应用无 glibc 强依赖、原生模块可在对应变体重编译 | musl/glibc 差异可能让原生模块运行期崩溃;tag 现行状态需核验 |
docker export | docker import 扁平化 | 仅应急压缩分发体积 | 丢失层历史与镜像配置,ENV/CMD/ENTRYPOINT 需重建,不作常规方案 |
什么时候不该用本文方案:单入口、产物与源码不可分离的应用(如直接 node src/index.js 运行),收窄 COPY 没有空间,此时应转向基础镜像降级;只想释放磁盘时 docker system prune 更直接,改 Dockerfile 解决不了磁盘问题。
三、关键原理
三条同时成立,多阶段才会更小:

- 最终阶段只
COPY产物与运行时依赖; - runtime 基础镜像确实比 builder 轻;
- 最终阶段自己不写粗粒度
COPY。
builder 阶段的 RUN npm ci 层默认不进入最终镜像,但 COPY --from=build /src /app 会把 /src 下的字节以最终阶段的一个新层永久计入——粒度是变量,不是常量。另外 .dockerignore 缺失在多阶段下通常是假根因:它影响的是发给构建器的上下文与 builder 层,只有最终阶段也写了 COPY . . 才计入体积。
还要分清三个口径:docker images 的 SIZE 是各层之和,共享基础层会被重复计入;docker system df 才接近这台机器的真实磁盘增量;registry 下载量是另一个口径,与本地解压后的 SIZE 不是同一个数。
四、可运行示例
环境:本机 docker 可用、能拉取 node:20-bookworm,目录含 package.json、package-lock.json 与 src/。
Dockerfile.bad:
# syntax=docker/dockerfile:1
FROM node:20-bookworm AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-bookworm AS runtime
WORKDIR /app
COPY --from=build /src /app
EXPOSE 3000
CMD ["node", "dist/server.js"]
Dockerfile.good:
# syntax=docker/dockerfile:1
FROM node:20-bookworm AS deps
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:20-bookworm AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-bookworm-slim AS runtime
WORKDIR /app
COPY --from=deps /src/node_modules ./node_modules
COPY --from=build /src/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
操作步骤:
docker build -t demo:bad -f Dockerfile.bad .
docker build -t demo:good -f Dockerfile.good .
docker images demo
docker history demo:bad --format 'table {{.Size}}\t{{.CreatedBy}}'
预期输出:docker history demo:bad 中出现一条体量显著的 COPY --from=build /src /app 层,其下还叠着 npm run build、COPY . .、RUN npm ci 的内容;demo:good 的 history 只剩 deps 与 dist 两路小拷贝,两者 docker images 的 SIZE 差值就是文题里 300MB 的来源(具体数字由本机实测产生)。
实际输出:请在你自己的环境执行上述命令,把 docker images demo 与 docker history 的表格贴回来对照;本文不预设任何未实测的 MB 数字。
常见失败及其原因与修复:按 Dockerfile.good 构建后容器启动报 Cannot find module './dist/server.js'——原因是 builder 阶段未产出 dist/,或 COPY --from=build /src/dist 的源路径与 package.json 里 build 脚本的输出目录不一致。修复:先 docker build --target build -t demo:build .,再 docker run --rm demo:build ls /src 确认产物路径,然后改 COPY 的源路径。另一常见失败是 runtime 换成 -alpine 后原生模块加载失败,此时回退 -slim 或在同变体内重编译。
五、验证结果与边界
- 已验证(可复现步骤见上一节):
docker history能定位到粗粒度COPY --from层;.dockerignore缺失在多阶段下不直接进入最终镜像。 - 未验证:
--format占位符名、-slim/-alpinetag 现行状态、npm ci --omit=dev与旧版写法的分界、registry 下载量的压缩口径、docker export丢失内容的官方表述——发布前逐条核验,核不到就删。 - 边界:
docker images的差值不等于磁盘多占的量(基础层共享),也不等于docker pull的下载量。构建缓存占用大与镜像大是两个指标。
思考
- 收窄
COPY增加一个deps阶段后,Dockerfile 的维护成本与 300MB 收益如何权衡? - 当团队无法区分"镜像体积"与"缓存占用"时,是否应先把指标口径写进发布清单?
参考资料
- Docker 官方文档 · Multi-stage builds
- Docker 官方文档 · Best practices for writing Dockerfiles
- Docker CLI 参考 · docker history
- Docker CLI 参考 · docker image inspect
- Docker CLI 参考 · docker system df
- Docker CLI 参考 · docker buildx du
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_71309014/article/details/170802576




