Docker 镜像为什么越拉越大:分层、缓存与构建顺序
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 压缩管用得多。
- 点赞
- 收藏
- 关注作者
评论(0)