余槐i头像
Docker 多阶段构建后镜像反而大 300MB:排查 COPY 顺序和缓存层封面图

Docker 多阶段构建后镜像反而大 300MB:排查 COPY 顺序和缓存层

本文摘要:多阶段构建上线后镜像大出三百兆,编译器明明已被挡在外面。逐层记账定位到粗粒度 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 解决不了磁盘问题。

三、关键原理

三条同时成立,多阶段才会更小:

正文技术图

  1. 最终阶段只 COPY 产物与运行时依赖;
  2. runtime 基础镜像确实比 builder 轻;
  3. 最终阶段自己不写粗粒度 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/-alpine tag 现行状态、npm ci --omit=dev 与旧版写法的分界、registry 下载量的压缩口径、docker export 丢失内容的官方表述——发布前逐条核验,核不到就删。
  • 边界:docker images 的差值不等于磁盘多占的量(基础层共享),也不等于 docker pull 的下载量。构建缓存占用大与镜像大是两个指标。

思考

  • 收窄 COPY 增加一个 deps 阶段后,Dockerfile 的维护成本与 300MB 收益如何权衡?
  • 当团队无法区分"镜像体积"与"缓存占用"时,是否应先把指标口径写进发布清单?

参考资料

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_71309014/article/details/170802576

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--