Docker 正在减肥

2025-06-07

Docker 正在减肥

Docker 容器在 CI 和 CD 方面非常有用。作为一名开发者,我对它的可能性和易用性感到惊讶。然而,当我开始使用 Docker 时,我并没有意识到自己完全搞错了。

经过几个月的构建和将容器推送到我们的私人 Nexus Registry 之后,我们的开发人员找到我说:“嘿,你的镜像占用了注册表的所有磁盘空间,可能需要检查一下它们是否有点过大?”。

臃肿的 docker 镜像也同样如此臃肿的 docker 镜像也同样如此

我有两个 Docker 镜像,一个用于 Python 应用程序,一个用于 Java 应用程序。它们的总大小高达 6.5GB(不含应用程序本身)。经过所有增强后,Python 镜像的大小为 1.4GB,Java 镜像的大小为 660MB(包含应用程序的二进制文件)。

在本文中,我将介绍优化 docker 镜像大小的技巧和窍门,这将节省您的注册表磁盘空间和(重新)部署时间。

创建基线

首先要做的是从图像中提取常见内容。

所有常用库和文件都应该放在基础镜像中,这样它们就会被缓存在 Docker 层中。就我而言,我将 Apache Thrift 和自定义日志轮换实用程序(从源代码构建)移到了基础镜像中,立即节省了 350MB 的空间。

关于层

Docker 镜像就像食人魔一样,它们有层Docker 镜像就像食人魔一样,它们有层

每个 Docker 镜像都由一系列层级构成。这些层级是使用 FROM、RUN、COPY 等命令对镜像进行的更改,您可以将其视为 Git 仓库中的提交。Docker 利用联合文件系统将这些层级合并为单个镜像。联合文件系统允许将不同文件系统(称为分支)的文件和目录透明地叠加,从而形成一个统一的文件系统。

留意临时文件

图层很方便,因为它们可以在构建或部署期间重复使用,但它们也可以使图像变得更大。

想象一下为 nodejs 应用程序创建图像:

看起来像是我们在 Bash 脚本里写的,对吧?但对 docker 来说完全不行。新镜像的大小是 248Mb。我们来看看docker history命令:

历史记录显示每层的大小历史记录显示每层的大小

输出显示我们有两倍的数据,每个都是 67.2MB。这是因为我们在一个层解压,在另一个层复制,而最后一个RUN命令中的rm对之前的层没有任何影响。因此,优化此示例的正确方法是:

我们所有的命令组合起来我们所有的命令组合起来

注意,还有一个rm -rf /var/lib/apt/lists/命令,它会清除 aptitude 的缓存。我们再看看docker 的历史记录

它明显更小它明显更小

不要落入抄袭陷阱

COPY命令用于在构建过程中将主机中的数据放入图像中。

COPY的缺点是它会创建一个单独的层,因此复制的数据将永远驻留在镜像中。复制存档或一些临时数据绝对不是一个好主意。如果可能的话,最好使用 wget(或其他工具)而不是 COPY,否则……

使用多阶段构建

多阶段构建是一项相对较新的功能,需要 Docker 17.05 或更高版本。此功能允许创建临时镜像,然后在构建最终镜像时使用其文件系统。其基本思路是安装所有依赖项,并在中间镜像中编译所有源文件,然后将结果复制到最终镜像中。我们来看一个示例:

这里我们安装apache thrift这里我们安装apache thrift

看起来我们有很多仅构建时的依赖项:

注意 apt-get 行注意 apt-get 行

现在我们添加另一个构建阶段并从“编译器”映像复制二进制文件:

注意“--from”参数注意“--from”参数

零钱和丰厚奖励:

少了 200Mb少了 200Mb

但要小心,不要删除包的运行时依赖项!

Java 运行时

你听说过jlink吗?它太棒了!你可以只用你需要的模块创建自己的 JRE,它应该比任何“精简”镜像都小。我建议使用AdoptOpenJDK镜像作为 jlink 的提供程序。思路是先用 jlink 创建 JRE,然后使用多阶段构建复制它:

“基线”图像指的是文章的第一部分“基线”图像指的是文章的第一部分

让我们看一下docker镜像

adoptopenjdk/openjdk11 x86_64-ubuntu-jdk-11.28       323MB
adoptopenjdk/openjdk11 jdk-11.0.5_10-ubuntu-slim     250MB
adoptopenjdk/openjdk11 x86_64-ubuntu-jre11u-nightly  127MB
custom-jre-build       jre11                         62.5MB
Enter fullscreen mode Exit fullscreen mode

使用 jlink,我们实现了比最小 AdoptOpenJDK 镜像小两倍的镜像大小。更不用说自定义 JRE 与基准镜像之间的集成非常便捷(一条简单的 COPY 指令)。

Java 依赖项

确实如此确实如此

大多数项目使用 Maven 或 Gradle 进行依赖管理。有时我们的依赖项非常多。在我们的项目中,依赖项多达 400MB!构建工具将所有依赖项的 jar 文件都放在主 jar 文件中,考虑到依赖项更新频率不高,我们会有很多具有相同依赖项的镜像。看起来非常适合迁移到其他层!

首先,您需要从项目中提取依赖项,如果它们已更新,则将它们推送到您的注册表。

让我们看看如何使用 Maven 和 SpringBoot 来实现。将excludeGroupIds参数添加到构建器插件配置中:

pom.xmlpom.xml

并使用这个小 bash 脚本:

现在,我们如何知道依赖项是否已更新以及是否需要推送它们?以及如何使用“依赖项镜像”作为应用程序镜像的基准?

我采用了一种 hack 的方法,计算 pom.xml 的 md5 哈希值,然后尝试从镜像仓库中拉取对应版本的 docker 镜像。如果镜像不存在,就创建并推送。我的做法如下:

构建 Java 依赖项 Docker 镜像构建 Java 依赖项 Docker 镜像

依赖项.dockerfile依赖项.dockerfile

接下来,我们通过ARG命令将依赖项哈希传递给应用程序的 dockerfile,并在FROM- image版本中使用它

docker build --build-arg version=${DEPENDENCIES_HASHES} -f application.dockerfile .
Enter fullscreen mode Exit fullscreen mode

应用程序.docker文件应用程序.docker文件

请注意,现在有一个 maven/gradle 插件jib,其工作与我上面写的类似(但不适用于我们的情况,因为我们的 Gitlab 运行器在每次构建后都会清除缓存)。

Python 特定

我使用了与 Java 容器相同的方法。我计算 requirements.txt 文件的哈希值,并将其用作中间容器的版本。

在 dockerfile 中,你应该使用多阶段构建。在第一阶段,安装所有构建依赖项,然后执行pip install --no-cache-dir。在结果阶段,从上一阶段复制 /opt/miniconda3 目录。

构建 Python 依赖项 Docker 镜像构建 Python 依赖项 Docker 镜像

我们来看一下 python-dependencies.dockerfile。别介意 apt-get install 部分,这些只是我的 Python 依赖项所需的包。

python-dependencies.dockerfilepython-dependencies.dockerfile

剩下要做的是根据依赖镜像创建最终的 Python 镜像。我只需复制所有 Python 脚本(以及一个用于启动 Python 的 Bash 脚本)。

应用程序.docker文件应用程序.docker文件

文章来源:https://dev.to/sammyvimes/docker-on-a-diet-1n6j
PREV
你应该知道的 10 个 JavaScript 基本面试题 4 是误解。正确答案是 == 用强制类型转换检查值,而 === 不用强制类型转换检查值(严格相等)。
NEXT
对初级开发人员的建议 1. 学习基础知识 2. 如果你没有计算机科学学位,不要担心 3. 如果你有计算机科学学位,不要让它影响你的理解 4. 这些是你需要学习/复习的主题 5. 当学习一门新技术时,要知道什么是可能的,并且知道在哪里查找它 6. 不要浪费时间掌握框架和库 7. 花时间享受你所处的位置