Docker 镜像为什么越拉越大:分层、缓存与构建顺序

举报
yd_228692151 发表于 2026/11/10 10:03:18 2026/11/10
【摘要】 Docker 镜像为什么越拉越大:分层、缓存与构建顺序一个 Go 服务编译出 12MB 的二进制,打成镜像却是 800MB。docker history 一看,罪魁祸首是基础镜像带了完整构建工具链,以及每次 COPY 源码都让后面所有层失效。分层机制不是黑盒,几条命令就能把账算清楚。 每一层都是完整的差异文件镜像由一组只读层叠加而成,容器启动时在最上面加一个可写层。关键在于:每一层的体积等...

Docker 镜像为什么越拉越大:分层、缓存与构建顺序

一个 Go 服务编译出 12MB 的二进制,打成镜像却是 800MB。docker history 一看,罪魁祸首是基础镜像带了完整构建工具链,以及每次 COPY 源码都让后面所有层失效。分层机制不是黑盒,几条命令就能把账算清楚。

每一层都是完整的差异文件

镜像由一组只读层叠加而成,容器启动时在最上面加一个可写层。关键在于:每一层的体积等于该层新增文件的大小,删除文件不会让层变小。

FROM golang:1.22
RUN dd if=/dev/zero of=/tmp/blob bs=1M count=200
RUN rm /tmp/blob

docker images 显示约 1GB,而文件明明删了。原因是 rm 只在上层写了一条 whiteout 记录,200MB 的 blob 仍完整存在于下层。真要减下去,必须让写入和删除落在同一个 RUN 里:

RUN dd if=/dev/zero of=/tmp/blob bs=1M count=200 && rm /tmp/blob

这条命令产生的层里没有 blob,因为中间状态从未被提交。这也是 apt-get install 之后紧跟 rm -rf /var/lib/apt/lists/* 的原因——分两条 RUN,反而比不删更占空间。

缓存命中的判定:指令文本 + 父层

构建缓存不是按文件内容比对,而是按指令字符串和父层 ID 做联合键。父层变了,后面全部 miss。所以顺序错了,缓存就等于没有。

COPY . .
RUN go mod download

改任何一个业务文件,COPY 层失效,go mod download 跟着重跑,依赖每次都重新下。换成先拷贝依赖清单:

COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app

只要 go.mod/go.sum 没变,go mod download 这一层就一直命中。同一台机器上,全量构建 74s,改一行代码后二次构建 6s,差别全在这一步。

.dockerignore 是同一件事的另一面。COPY . . 会把 .git、node_modules、测试报告一起塞进去,既撑大层体积,又让任何日志文件变动都触发失效。写清楚 ignore 规则,构建时间和镜像大小都会降。

多阶段构建解决"构建依赖被带进运行时"

编译需要 gcc、头文件、包管理器,运行只需要一个二进制。多阶段构建把它们分开:

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server

FROM gcr.io/distroless/static
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

--from=build 只拷贝显式指定的文件,构建阶段的工具链不进最终镜像。上面的写法最终镜像约 15MB,而单阶段版本是 850MB 左右。-ldflags="-s -w" 去掉符号表和调试信息,还能再省几 MB。

顺手查账的三个命令

docker history --no-trunc <image>        # 每层指令与体积,找出大层
docker image inspect <image> --format '{{len .RootFS.Layers}}'   # 层数
docker build --progress=plain . 2>&1 | grep -E "CACHED|DONE"     # 哪些层命中缓存

docker history 输出里 0B 的行是 ENV、CMD 这类元数据层,不占空间;几百 MB 的 ADD/RUN 才是目标。想逐层看文件,docker save 出 tar 后逐层解开比对即可。

几条容易踩反的规则

  • ADD 能解压能拉远程,COPY 不能,可预期性更重要,除了解压场景一律用 COPY。
  • ARG 变化会击穿其后所有层的缓存,把版本号作为 ARG 放在依赖安装之前,等于每次升级都全量重建;放在 RUN 之后再利用。
  • 层数不是越少越好,合并十几条不相关的 RUN 会让每次微调都重建整块,代价比多几层更高。
  • 镜像 tag 用固定版本,:latest 会让"同一份 Dockerfile"在不同时间构建出不同的镜像,缓存也随之不可复现。

镜像体积和构建速度本质上是同一件事:让不该进镜像的文件不要进层,让不该失效的层不要失效。想清楚哪一步该被缓存、哪一步会变,比事后用 docker-squash 压缩管用得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。