在使用 Docker 构建和部署应用程序时,镜像体积的大小会直接影响构建速度、传输效率以及存储成本。随着项目的复杂化和依赖项的增加,Docker 镜像的体积往往会逐渐增大,这可能导致部署速度变慢、带宽消耗增加,甚至在资源受限的环境中出现问题。因此,优化 Docker 镜像体积成为了一个重要的课题。
本文将分析导致 Docker 镜像体积变大的主要原因,并针对不同的场景,提供有效的优化策略。
一、为什么 Docker 镜像体积会变大?
1. 过多的层次结构
Docker 镜像由多个只读层组成,每个指令(如 RUN、COPY、ADD)都会创建一个新的层。如果 Dockerfile 中存在大量的指令,或者频繁执行修改文件系统的操作(如安装软件包、复制文件等),就会生成大量的层,导致镜像体积膨胀。
2. 未清理的临时文件和缓存
在镜像构建过程中,如果不及时清理临时文件和缓存数据,这些无用的文件会占据大量空间,直接增加镜像的体积。例如,使用 apt-get 安装软件包时,未清理的包管理器缓存可能会占据几十到几百兆的空间。
3. 不必要的依赖项和工具
有时为了方便开发,我们可能会在镜像中安装各种工具和依赖项,但这些工具在最终的生产环境中并不需要。如果没有对这些不必要的内容进行剔除,也会导致镜像体积过大。
4. 基础镜像选择不当
不同的基础镜像有不同的体积,选择一个功能过多的基础镜像(如 ubuntu 或 debian)可能会引入许多用不到的组件,而这些组件会大大增加镜像体积。
二、如何分析每一层的大小?
分析 Docker 镜像中每一层的大小可以帮助你识别出哪些指令导致了镜像体积的增长,从而进行有针对性的优化。以下是几种常用的方法来分析 Docker 镜像每一层的大小:
1. 使用 docker history 命令
docker history 命令可以显示镜像的历史记录,包括每一层的大小、创建时间和对应的 Dockerfile 指令。docker history <image_name>输出示例:
docker history jenkins/jenkinsIMAGE CREATED CREATED BY SIZE COMMENTdd85f58e0acc 7 weeks ago LABEL org.opencontainers.image.vendor=Jenkin… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENTRYPOINT ["/usr/bin/tini" "--" "/usr/local… 0B buildkit.dockerfile.v0<missing> 7 weeks ago COPY jenkins-plugin-cli.sh /bin/jenkins-plug… 323B buildkit.dockerfile.v0<missing> 7 weeks ago COPY jenkins.sh /usr/local/bin/jenkins.sh # … 2.32kB buildkit.dockerfile.v0<missing> 7 weeks ago COPY jenkins-support /usr/local/bin/jenkins-… 6.5kB buildkit.dockerfile.v0<missing> 7 weeks ago USER jenkins 0B buildkit.dockerfile.v0<missing> 7 weeks ago COPY /javaruntime /opt/java/openjdk # buildk… 91.8MB buildkit.dockerfile.v0<missing> 7 weeks ago ENV PATH=/opt/java/openjdk/bin:/usr/local/sb… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV JAVA_HOME=/opt/java/openjdk 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV COPY_REFERENCE_FILE_LOG=/var/jenkins_hom… 0B buildkit.dockerfile.v0<missing> 7 weeks ago EXPOSE map[50000/tcp:{}] 0B buildkit.dockerfile.v0<missing> 7 weeks ago EXPOSE map[8080/tcp:{}] 0B buildkit.dockerfile.v0<missing> 7 weeks ago RUN |15 TARGETARCH=arm64 COMMIT_SHA=5ba79515… 6.89MB buildkit.dockerfile.v0<missing> 7 weeks ago ARG PLUGIN_CLI_URL=https://github.com/jenkin… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG PLUGIN_CLI_VERSION=2.13.0 0B buildkit.dockerfile.v0<missing> 7 weeks ago RUN |13 TARGETARCH=arm64 COMMIT_SHA=5ba79515… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV JENKINS_INCREMENTALS_REPO_MIRROR=https:/… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV JENKINS_UC_EXPERIMENTAL=https://updates.… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV JENKINS_UC=https://updates.jenkins.io 0B buildkit.dockerfile.v0<missing> 7 weeks ago RUN |13 TARGETARCH=arm64 COMMIT_SHA=5ba79515… 93.3MB buildkit.dockerfile.v0<missing> 7 weeks ago ARG JENKINS_URL=https://repo.jenkins-ci.org/… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG JENKINS_SHA=6334c70dcfb4ef0815387bffb83d… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV JENKINS_VERSION=2.466 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG JENKINS_VERSION=2.466 0B buildkit.dockerfile.v0<missing> 7 weeks ago RUN |10 TARGETARCH=arm64 COMMIT_SHA=5ba79515… 0B buildkit.dockerfile.v0<missing> 7 weeks ago VOLUME [/var/jenkins_home] 0B buildkit.dockerfile.v0<missing> 7 weeks ago RUN |10 TARGETARCH=arm64 COMMIT_SHA=5ba79515… 4.41kB buildkit.dockerfile.v0<missing> 7 weeks ago ENV REF=/usr/share/jenkins/ref 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV JENKINS_SLAVE_AGENT_PORT=50000 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV JENKINS_HOME=/var/jenkins_home 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG REF=/usr/share/jenkins/ref 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG JENKINS_HOME=/var/jenkins_home 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG agent_port=50000 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG http_port=8080 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG gid=1000 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG uid=1000 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG group=jenkins 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG user=jenkins 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG COMMIT_SHA=5ba795152d060b18bf3d25a6664c0… 0B buildkit.dockerfile.v0<missing> 7 weeks ago ARG TARGETARCH=arm64 0B buildkit.dockerfile.v0<missing> 7 weeks ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0<missing> 7 weeks ago RUN /bin/sh -c curl -s https://packagecloud.… 16.1MB buildkit.dockerfile.v0<missing> 7 weeks ago RUN /bin/sh -c apt-get update && apt-get i… 151MB buildkit.dockerfile.v0<missing> 2 months ago /bin/sh -c #(nop) CMD ["bash"] 0B<missing> 2 months ago /bin/sh -c #(nop) ADD file:cfc8f76c8181d3ae6… 139MB
SIZE 列:显示了每一层的大小。
CREATED BY 列:显示了创建该层的 Dockerfile 指令。
IMAGE 列:在
docker history输出中,<missing>通常出现在IMAGE列,表示该层没有直接对应的唯一镜像 ID。这种情况通常发生在以下几种情况下:
多阶段构建:在多阶段构建中,某些层可能被丢弃或没有被最终保留下来。
Layer Squashing:当使用了
--squash等方法压缩 Docker 镜像的层时,原来的层可能会失去其单独的镜像 ID。BuildKit 的影响:BuildKit 可能会优化和重新组织层的顺序,导致某些中间层的 ID 显示为
<missing>。镜像层的再利用:Docker 在构建时可能重用某个层,如果最终镜像中没有直接引用这个层,它的镜像 ID 也可能会显示为
<missing>。
<missing> 表示该层没有一个单独的镜像 ID,可能是因为它被丢弃、压缩、优化,或者被再利用。如果我们看到这个标识,不必担心,这只是反映了 Docker 构建过程中对镜像层的处理方式,并不影响镜像的正常使用。2. 使用 dive 工具
dive 是一个开源工具,用于分析 Docker 镜像的层结构和大小,支持交互式浏览镜像的每一层。它可以直观地显示每一层的文件变更情况和大小,帮助你识别不必要的文件和层。
使用 dive 分析镜像:
dive jenkins/jenkinsImage Source: docker://jenkins/jenkinsFetching image... (this can take a while for large images)
笔者也在熟悉 dive 工具中,大家可以参考项目官网:https://github.com/wagoodman/dive。
3. 使用 docker inspect 命令
docker inspect 命令可以显示 Docker 镜像的详细信息,包括每一层的 ID 和大小。虽然它不如 dive 直观,但可以结合 docker history 提供的信息进行更深层次的分析。
docker inspect <image_name>docker inspect jenkins/jenkins[{"Id": "sha256:dd85f58e0acc88158e5f22ce1d444a091f5119a9e71434c965873f6db1098737","RepoTags": ["jenkins/jenkins:latest"],"RepoDigests": ["jenkins/jenkins@sha256:f2e76ce1ba8d7b357f79a6f174b6696d3ede0cd9c323c5bd7347bf945807ac29"],"Parent": "","Comment": "buildkit.dockerfile.v0","Created": "2024-07-02T19:12:02.480223349Z","ContainerConfig": {"Hostname": "","Domainname": "","User": "","AttachStdin": false,"AttachStdout": false,"AttachStderr": false,"Tty": false,"OpenStdin": false,"StdinOnce": false,"Env": null,"Cmd": null,"Image": "","Volumes": null,"WorkingDir": "","Entrypoint": null,"OnBuild": null,"Labels": null},"DockerVersion": "","Author": "","Config": {"Hostname": "","Domainname": "","User": "jenkins","AttachStdin": false,"AttachStdout": false,"AttachStderr": false,"ExposedPorts": {"50000/tcp": {},"8080/tcp": {}},"Tty": false,"OpenStdin": false,"StdinOnce": false,"Env": ["PATH=/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","LANG=C.UTF-8","JENKINS_HOME=/var/jenkins_home","JENKINS_SLAVE_AGENT_PORT=50000","REF=/usr/share/jenkins/ref","JENKINS_VERSION=2.466","JENKINS_UC=https://updates.jenkins.io","JENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimental","JENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementals","COPY_REFERENCE_FILE_LOG=/var/jenkins_home/copy_reference_file.log","JAVA_HOME=/opt/java/openjdk"],"Cmd": null,"Image": "","Volumes": {"/var/jenkins_home": {}},"WorkingDir": "","Entrypoint": ["/usr/bin/tini","--","/usr/local/bin/jenkins.sh"],"OnBuild": null,"Labels": {"org.opencontainers.image.description": "The Jenkins Continuous Integration and Delivery server","org.opencontainers.image.licenses": "MIT","org.opencontainers.image.revision": "5ba795152d060b18bf3d25a6664c047051cabda5","org.opencontainers.image.source": "https://github.com/jenkinsci/docker","org.opencontainers.image.title": "Official Jenkins Docker image","org.opencontainers.image.url": "https://www.jenkins.io/","org.opencontainers.image.vendor": "Jenkins project","org.opencontainers.image.version": "2.466"}},"Architecture": "arm64","Os": "linux","Size": 497797573,"GraphDriver": {"Data": {"LowerDir": "/var/lib/docker/overlay2/6befaa47637e414c0c4e4ab9219f8054bed040c785f41f586fdce5e626ac5a56/diff:/var/lib/docker/overlay2/b9f076ee9d302524244d6edcc06e1dbffba4a319f0e8e59a4d51b0a197073b09/diff:/var/lib/docker/overlay2/864a46d1322aec44e72a86b850dde311d3e76f4f05e280e611337761a52002f3/diff:/var/lib/docker/overlay2/616d08668e15fb162db85ec3a73e2b3bc17da3fa4147cbce97a430f1cd056b34/diff:/var/lib/docker/overlay2/d3122e80afe5c0525427c56d7ba0c0d82988920eb9843a5a4b48f613565d1bb4/diff:/var/lib/docker/overlay2/4dde7740a1847f6eded9fa4ecd108603ccf5597707b84a4f32311a860c62373d/diff:/var/lib/docker/overlay2/00fefc3ada160cad8aff44effc65721f65df644207ed3b9fa83ed1895bd5c2f6/diff:/var/lib/docker/overlay2/ca4734537d03cc367e4b9f42b8fc903ece48d9fd2dbae473bc4f3783e6ee5bfc/diff:/var/lib/docker/overlay2/6693d4c301718c4e149621425e33d1124e2d61c9e56e72532714056941a97df6/diff:/var/lib/docker/overlay2/a3423a1c34e2f0b84a7734f17e4e15aa74d80ae4e9dd773f2bd6eb681d9f5ff6/diff:/var/lib/docker/overlay2/4d41760242b8c752a5b79938292cc4a2a1740f7a92a0db5f8b57133b41c3234e/diff","MergedDir": "/var/lib/docker/overlay2/0ae1145e0982f25d0f7898f0a471fe61f7d1caa0376519824b969739188f9d7a/merged","UpperDir": "/var/lib/docker/overlay2/0ae1145e0982f25d0f7898f0a471fe61f7d1caa0376519824b969739188f9d7a/diff","WorkDir": "/var/lib/docker/overlay2/0ae1145e0982f25d0f7898f0a471fe61f7d1caa0376519824b969739188f9d7a/work"},"Name": "overlay2"},"RootFS": {"Type": "layers","Layers": ["sha256:bf6ea8abb3429efa5adc5f3f30017f84290714ca307db7952c98474b26606b4e","sha256:cd07e958570460f470c690037fe240d92302d4b3aeb8b0b7f354287b6c5896b7","sha256:ce61f8fc5ceca1f41133373ea4566e05ee40c9c06885467dd4b00b685a07b524","sha256:d11019f993142e64cb796eb7b2e78520ded2826ba12afd0bd13fa5ec8777e6d2","sha256:2374b1dd8b323b267ef7f04631c4b93d7ef8ac8220a60153f0fe61a935bb1886","sha256:6645e5591e03ab36c20c6061dc40a8034e12b7126d3ff6803f98b90a075c38b8","sha256:abca06823e29f50c36f6148ae24f57717ec92ac1b0c2a707f25746e4ade8b604","sha256:dfb5eb9ae52a69b2e96fa9f0be6b101e057dd96da77ab030b24e3329af9ca692","sha256:bd6b48f477d9b347284160d676ba6f3c782232720d10872abf6fe7343d18dcb6","sha256:62623c201ac7c19bba8766d7b422c75d4d90147b6f1f98de295b036296ea12b4","sha256:244bf2e1b68e900b972594b707472c394b27f9acc3e24f8364e126a924ce1360","sha256:aab2a919a4be7aa2f19ad058825213818ddfb1c13a614ca8ccb4817e21ac0938"]},"Metadata": {"LastTagTime": "0001-01-01T00:00:00Z"},"Container": ""}]
三、不同场景的优化策略
1. 合并 Dockerfile 中的指令
问题: 过多的层次结构。
解决方案: 将多个命令合并到一个 RUN 指令中,以减少镜像的层数。例如:
# 优化前RUN apt-get updateRUN apt-get install -y curlRUN apt-get install -y vim# 优化后RUN apt-get update && \apt-get install -y curl vim && \rm -rf /var/lib/apt/lists/*
通过合并命令,不仅减少了层数,还可以在同一个层中清理缓存文件,进一步减少镜像体积。
2. 选择轻量级的基础镜像
问题: 基础镜像选择不当。
解决方案: 根据实际需求选择合适的轻量级基础镜像,如 alpine、busybox 等。这些镜像体积较小,通常只有几兆到几十兆,而常见的 ubuntu 镜像可能达到几百兆。
例如,将基础镜像从 ubuntu 替换为 alpine:
# 原始镜像FROM ubuntu:20.04# 优化后的镜像FROM alpine:3.12
通过切换到 alpine,可以显著减少镜像体积,前提是 alpine 能满足您的应用程序依赖需求。
社区提供的很多镜像都有 slim 版本,我们也可以评估是否能够满足需求。
此外我们还会用到:
FROM scratch# scratch 是一个特殊的镜像,实际上它并不是真正的镜像,而是一个标识,表示不使用任何基础镜像。这意味着构建的镜像将从一个完全空白的状态开始,没有任何操作系统层或工具。
`FROM scratch` 的用途和特点
最小化镜像体积:
因为
scratch没有任何内容,所以使用它可以创建非常小的镜像。常用于只包含一个静态编译的二进制文件的容器,特别是在 Go、Rust 等语言的应用中,编译后的二进制文件不依赖于外部库。由于没有基础镜像,我们无法在容器内使用常见的 Linux 工具,如
bash、sh、ls等。如果需要调试,必须手动将调试工具放入镜像中。最大化安全性:
由于没有多余的操作系统工具或库,这种镜像的攻击面极小,提升了安全性。
手动构建镜像:
使用
scratch构建镜像时,开发者需要手动处理所有依赖。这意味着必须将所有需要的文件(如二进制文件、配置文件、依赖库等)通过COPY或ADD指令手动添加到镜像中。如果我们的应用依赖某些系统库或工具(如 glibc、libssl 等),需要手动将这些依赖添加到镜像中,这对构建过程要求较高。
适用于特定场景:
scratch 主要适用于那些能够静态编译并且不依赖系统库的应用。对于复杂的应用程序,可能不适合从 scratch 开始构建。
3. 清理临时文件和缓存
问题: 未清理的临时文件和缓存。
解决方案: 在安装软件包或执行命令后,及时清理不必要的文件。例如,清理包管理器的缓存,删除编译生成的中间文件等:
RUN apt-get update && \apt-get install -y --no-install-recommends build-essential && \make && make install && \apt-get purge -y --auto-remove build-essential && \rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
这种方法可以确保镜像中只保留必要的文件和数据。
4. 多阶段构建
问题: 不必要的依赖项和工具。
解决方案: 使用多阶段构建,将构建工具和依赖项与最终的运行环境隔离开来。多阶段构建允许你在一个 Dockerfile 中定义多个构建阶段,从而只在最终镜像中保留必要的文件和依赖。
# 第一阶段:构建应用程序FROM golang:1.16 AS builderWORKDIR /appCOPY . .RUN go build -o myapp .# 第二阶段:创建运行时镜像FROM alpine:3.12WORKDIR /appCOPY --from=builder /app/myapp .CMD ["./myapp"]
在这个例子中,所有编译工具和依赖项都留在了 builder 阶段,而最终的运行时镜像只包含编译后的二进制文件,极大地减少了镜像的体积。
5. 手动优化镜像
问题: 复杂的层次结构和历史累积。
解决方案: 如果镜像已经生成,并且需要进一步优化,可以使用 docker export 和 docker import 命令将镜像的内容导出后再导入。这种方式将所有层合并为一个,消除了不必要的中间层:
docker export $(docker create my-image) | docker import - my-optimized-image通过这种方式生成的镜像只有一个层,体积更加紧凑。
四、总结
Docker 镜像体积的优化是一项重要的任务,能够提高应用程序的部署效率、节省存储空间,并降低带宽消耗。通过合并 Dockerfile 指令、选择轻量级基础镜像、清理临时文件、使用多阶段构建以及手动优化镜像,可以有效地减少 Docker 镜像的体积。同时,掌握分析每一层大小的方法,能够帮助你更精确地定位并优化影响镜像体积的因素。




