docker镜像体积优化攻略参考—— 筑梦之路
简单介绍镜像的本质是镜像层和运行配置文件组成的压缩包构建镜像是通过运行 Dockerfile 中的RUN、COPY和ADD等指令生成镜像层和配置文件的过程。和镜像体积大小有关的关键点RUN、COPY和ADD指令会在已有镜像层的基础上创建一个新的镜像层执行指令产生的所有文件系统变更会在指令结束后作为一个镜像层整体提交。• 镜像层具有copy-on-write的特性如果去更新其他镜像层中已存在的文件会先将其复制到新的镜像层中再修改造成双倍的文件空间占用。• 如果去删除其他镜像层的一个文件只会在当前镜像层生成一个该文件的删除标记并不会减少整个镜像的实际体积。验证上面的结论过程如下cat Dockerfile FROM alpine:latest COPY resource.tar / RUN touch /resource.tar RUN rm -f /resource.tar ENTRYPOINT [/bin/ash] # 构建镜像并查看层 $ docker build -t test-image -f Dockerfile . $ docker history test-image:latest IMAGE CREATED CREATED BY SIZE COMMENT 95f1695b2904 About a minute ago /bin/sh -c #(nop) ENTRYPOINT [/bin/ash] 0B 1780448c656f About a minute ago /bin/sh -c rm -f /resource.tar 0B a85d29bf7738 About a minute ago /bin/sh -c touch /resource.tar 135MB 6dac335fa653 4 minutes ago /bin/sh -c #(nop) COPY file:66065d6e23e0bc52… 135MB e66264b98777 7 weeks ago /bin/sh -c #(nop) CMD [/bin/sh] 0B missing 7 weeks ago /bin/sh -c #(nop) ADD file:8e81116368669ed3d… 5.53MB在docker history的输出结果中可以看到RUN touch /resource.tar指令只是修改了文件的元信息但依然将整个文件拷贝到了新的镜像层中。RUN rm -f /resource.tar指令虽然删除了文件并且该文件在运行容器时不可见但依然在前两个镜像层中以及最终的镜像中存在。常用分析工具1. docker historydocker 自带的docker history命令该命令可以展示所有镜像层的创建时间、指令以及体积等较为基础的信息但对于复杂的镜像则有些乏力。2. dive第三方的dive工具该工具可以分析镜像层组成并列出每个镜像层所包含的文件列表可以很方便地定位到影响镜像体积的构建指令以及具体文件如何优化1. 分阶段构建与从零构建分阶段构建multi-stage builds和从零构建build from scratch是优化镜像体积的基本手段和必备技巧。该技巧将镜像构建过程区分为构建和运行环境在构建环境安装编译器等依赖并编译所需的二进制包然后将其复制到仅包含必要运行依赖的运行环境中。对 golang 这类能够编译静态二进制文件的语言来说分阶段构建的效果尤为明显我们可以将编译产生的二进制文件放到scratch镜像中运行scratch是一个特殊的空镜像FROM golang COPY hell0.go . ENV CGO_ENABLED0 RUN go build hello.go FROM scratch COPY --from0 /go/hello . CMD [./hello]如果直接使用 golang 镜像作为运行环境其镜像体积通常接近 1 个 G其中大部分文件都不是在运行容器时所必要的。将编译结果拷贝到运行环境后体积只有几十 kb~mb 不等如果需要在运行容器中保留基本的系统工具可以考虑使用 alpine 镜像作为运行环境。关于分阶段构建和从零构建的更多细节可参考 Docker 官方文档中的 Use multi-stage builds 和 Create a simple parent image using scratch。2. 避免产生无用的文档或缓存docker 镜像不应该包含文档、缓存等对运行容器没有作用的内容。避免在本地保留安装缓存。大部分包管理器会在安装时缓存下载的资源以备之后使用以 pip 为例会将下载的响应和构建的中间文件保存在~/.cache/pip目录应使用--no-cache-dir选项禁用默认的缓存行为。避免安装文档。部分包管理器提供了选项可以不安装附带的文档如 dnf 可使用--nodocs选项。避免缓存包索引。部分包管理器在执行安装之前会尝试查询所有已启用仓库的包列表、版本等元信息缓存在本地作为索引。个别仓库的索引缓存可达到 150 M 以上。我们应该仅在安装包时查询索引并在安装完成后清理不应该在单独的指令中执行yum makecache这类缓存索引的命令3. 及时清理不需要的文件运行容器时不需要的文件一定要在创建的同一层清理否则依然会保留在最终的镜像中。通过包管理安装包通常会产生大量的缓存文件一定要在同一RUN指令的结尾处立刻清理。在安装依赖数量较多时可以节省大量的缓存空间。# dnf RUN dnf install -y --nodocs PACKAGES \ dnf clean all \ rm -rf /var/cache/dnf # apt RUN apt-get update \ apt-get install -y PACKAGES \ rm -rf /var/lib/apt/lists/* # 官方的 ubuntu/debian 镜像 apt-get 会在安装后自动执行 clean 命令3. 合并多个镜像层应该避免在不同镜像层中更新文件而造成额外的体积占用。当构建的层数很多且执行指令较复杂时很难避免在不同的镜像层中更新文件可通过以下手段精简这部分额外体积1) 在最终生成镜像时将所有镜像层合并成一层在docker build命令中使用—squash即可实现需要开启 docker daemon 的实验性功能docker build -t squash-image --squash -f Dockerfile .最终生成的镜像只有一个镜像层包含最后实际存在的文件系统在合并所有镜像层的过程中相当于禁用了copy-on-write特性。这种做法的坏处在于镜像在保存和分发时是可以复用镜像层的推送镜像时会跳过镜像仓库已存在的镜像层拉取镜像时会跳过本地已拉取过的镜像层而合并成一层后则失去了这种优势2)分阶段构建将部分中间镜像层压缩成一层作为基础镜像。在开发团队内部我们往往会在官方镜像的基础上添加或更新部分依赖然后作为团队内部统一使用的基础镜像这种复用方式可以大大减少实际占用的镜像体积。更进一步我们可以将这类基础镜像压缩成一层FROM golang:1.16 as base FROM scratch COPY --frombase / / ENTRYPOINT [/bin/bash]压缩成一层后golang:1.16的镜像体积从 919MB 变成 913MB官方镜像已经做了很多优化所以节省空间十分有限但对于开发团队内部制作的基础镜像这种优化往往会带来意外惊喜4. 复制文件的同时修改元信息先将文件添加到镜像内然后再修改文件的执行权限和所属用户这类 COPY-RUN 指令在 Dockerfile 中十分常见:COPY output/hello /usr/bin/hello RUN chmod x /usr/bin/hello chown normal:normal /usr/bin/hello但修改文件元信息也会将文件复制到新的镜像层以上指令会产生两份相同的文件。在文件体积较大时会显著增加整个镜像的体积。事实上我们可以在复制文件的同时完成对文件元信息的修改COPY和ADD指令都提供了修改元信息的--chmod和--chown选项:COPY --chmod755 --chownnormal:normal output/hello /usr/bin/hello--chmod特性目前还未添加到官方文档使用前需要开启 docker 的 buildkit 特性在docker build命令前添加DOCKER_BUILDKIT1即可目前只支持--chmod755和--chmod0755这种设置方法不支持--chmodx。注经测试当使用ADD指令且源文件为下载链接时--chmod选项不起作用不清楚这是 docker 的 bug 还是 feature。解决方案是直接使用RUN指令wget chmod来替代ADD本文搜集来自镜像体积从1000M到10M几招就做到仅供参考。