如何在 Docker 中优化容器镜像大小

大家好!我是大聪明-PLUS

介绍

当我开始将我的博客搜索服务容器化时,我反复修改 Dockerfile,学习如何构建镜像。容器化本身并不难,但我想要创建一个尽可能小巧高效的镜像,而这个过程比我预想的要复杂一些。下面,我想更详细地分享我在编写这个 Dockerfile 时学到的东西,这个 Dockerfile 特别注重最终镜像的大小。

我会展示各种 Dockerfile 版本进行比较,并在文章末尾提供一个表格,列出每个文件生成的镜像大小。这将帮助您更轻松地评估每项优化的效果。

服务示例

在探索 Dockerfile 示例之前,让我们先深入了解一下单个镜像的结构。例如,我们将查看一个小型 Python 服务。使用时,我们无需搜索文件,只需查看 Dockerfile 的迭代即可。这个 Web 服务基于 gunicorn(Falcon API),从功能角度来看非常简单------它只负责发送"你好"之类的消息。

依赖关系

首先,让我们设置所有需要的 Python 依赖项。

requirements.txt

复制代码
Plain-Text-Markdown-Extention` `@` `git+https`:`//github`.`com/kostyachum/python-markdown-plain-text`.`git#egg=plain-text-markdown-extention`
`falcon`
`gunicorn

我愿意假装这项服务需要"纯文本 Markdown 扩展",即使它并非必需。在这个例子中,我只是想演示我们需要使用 pip 从 GitHub 安装依赖项。这确保了 git 作为构建依赖项被拉取到镜像中。所以,让我们假装它是必需的,因为它模拟了一个真实的实际情况。

服务

接下来我们来看一个非常简单的演示服务。
ex_serv.py

复制代码
#!/usr/bin/env python`

`import` `falcon`

`class` `ExResource`:

    `def` `on_get`(`self`, `req`, `resp`):
        `resp`.`status` `=` `falcon`.`HTTP_200`
        `resp`.`text` `=` `"hi"`

`app` `=` `falcon`.`App`()
`exr` `=` `ExResource`()
`app`.`add_route`(`'/'`, `exr`)`

优化

在这种情况下,最重要的任务是选择合适的基础镜像,因为它的大小最终决定了最终镜像的大小。

其次,虽然重要性稍逊,但同样重要的是将构建镜像和发布镜像分成两个阶段,分别包含在各自的 Dockerfile 中。Dockerfile 必须同时容纳构建镜像和发布镜像,这一点至关重要。

选择基本图像

在开发这项服务之初,我选择了"python:3.11"镜像作为基础镜像,因为我觉得它最合适。我正在编写一个Python应用程序,所以Python镜像非常理想。然而,这个镜像需要完整的Debian系统安装,因此体积相当大。

之后,我尝试了"python:3.11-slim"镜像,它同样基于Debian,但体积更小。它比之前的镜像小了很多,但仍然超出了我的预期。

接下来,我尝试了"python:3.11-alpine"镜像,结果相当不错。它生成了一个紧凑且可以直接使用的镜像。但我认为它还可以做得更小。

所以我尝试使用"alpine:latest"镜像并自行安装Python。这很有意思,因为如果不将镜像拆分为构建版和发布版,最终生成的镜像比"python:3.11-alpine"版本还要大。但当镜像拆分为构建版和发布版后,镜像体积反而更小了。

比较单张镜像和分为构建版和发布版的镜像。

让我们来看看准备一个统一且独立的图像需要做哪些工作。

单张图片

当我们创建单个镜像时,所有依赖项都会被安装并保留在镜像中。这尤其适用于构建依赖项,例如 Git。最终生成的 Dockerfile 非常简单,但镜像中仍然包含一些并非运行服务直接必需的组件。

没错,这只是一个非常简单的例子,但如果您仔细阅读镜像描述部分,就会发现像 Git 这样的细节对安装的影响有多大。这主要是因为 Git 会自动拉取大量的依赖项git

分割图像

创建拆分镜像时,首先会准备其"构建"部分,安装所有依赖项,包括构建所需的依赖项。然后,在这个镜像中构建应用程序本身。但由于我们处理的是一个简单的 Python 应用程序,因此构建步骤本身对我们来说并不重要。

应用程序构建完成后,会创建第二个镜像,即"发布"镜像,并将依赖项从"构建"镜像复制到其中。我们的服务也使用完全相同的原理进行复制。"发布"镜像定义了我们想要提供的所有配置。例如,它定义了服务默认监听的端口以及启动 gunicorn 的命令。

最后,在构建过程的某个阶段,Docker 会丢弃"构建"镜像,因为只保留 Dockerfile 中定义的最终镜像。

拆分镜像稍微复杂一些。但是,它不会将 git、依赖项或 requirements.txt 文件本身拉取到镜像的发布部分。这带来的区别比看起来要重要得多------即使对于像我们这样小而简单的项目也是如此。

Dockerfile 选项

python:3.11

单图

复制代码
FROM` `python`:`3.11`
`EXPOSE` `80`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apt-get` `install` `-y` `git`
`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`

`COPY` .`/ex_serv`.`py` .

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]`

分割图像

复制代码
FROM` `python`:`3.11` `AS` `build`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apt-get` `install` `-y` `git`

`RUN` `python3` `-m` `venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`
`RUN` `pip3` `uninstall` `-y` `pip` `setuptools` `packaging`

`FROM` `python`:`3.11` `AS` `release`
`EXPOSE` `80`
`WORKDIR` `/app`

`COPY` .`/ex_serv`.`py` .

`COPY` `--from=build` `/opt/venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]`

要准备一个精简镜像,您需要运行命令**,** slim apt-get update该命令会填充软件包管理器使用的文件列表。否则,尝试安装时会出错git

单镜像

复制代码
FROM` `python`:`3.11-slim`
`EXPOSE` `80`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apt-get` `update`
`RUN` `apt-get` `install` `-y` `git`
`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`

`COPY` .`/ex_serv`.`py` .

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]
`

分割图像

复制代码
FROM` `python`:`3.11-slim` `AS` `build`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apt-get` `update`
`RUN` `apt-get` `install` `-y` `git`

`RUN` `python3` `-m` `venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`
`RUN` `pip3` `uninstall` `-y` `pip` `setuptools` `packaging`

`FROM` `python`:`3.11-slim` `AS` `release`
`EXPOSE` `80`
`WORKDIR` `/app`

`COPY` .`/ex_serv`.`py` .

`COPY` `--from=build` `/opt/venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]`

python:3.11-alpine

单图

复制代码
FROM` `python`:`3.11-alpine`
`EXPOSE` `80`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apk` `add` `--no-cache` `git`
`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`

`COPY` .`/ex_serv`.`py` .

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]`

分割图像

复制代码
FROM` `python`:`3.11-alpine` `AS` `build`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apk` `add` `--no-cache` `git`

`RUN` `python3` `-m` `venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`
`RUN` `pip3` `uninstall` `-y` `pip` `setuptools` `packaging`

`FROM` `python`:`3.11-alpine` `AS` `release`
`EXPOSE` `80`
`WORKDIR` `/app`

`COPY` .`/ex_serv`.`py` .

`COPY` `--from=build` `/opt/venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]`

alpine:最新

单张图片

复制代码
FROM` `alpine`:`latest`
`EXPOSE` `80`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apk` `add` `--no-cache` `git` `python3` `py3-pip`

`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`

`COPY` .`/ex_serv`.`py` .

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]`

分割图像

复制代码
FROM` `alpine`:`latest` `AS` `build`
`WORKDIR` `/app`

`COPY` .`/requirements`.`txt` .

`RUN` `apk` `add` `--no-cache` `git` `python3` `py3-pip`

`RUN` `python3` `-m` `venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`RUN` `pip3` `install` `--no-cache-dir` `-r` `requirements`.`txt`
`RUN` `pip3` `uninstall` `-y` `pip` `setuptools` `packaging`

`FROM` `alpine`:`latest` `AS` `release`
`EXPOSE` `80`
`WORKDIR` `/app`

`RUN` `apk` `add` `--no-cache` `python3`

`COPY` .`/ex_serv`.`py` .

`COPY` `--from=build` `/opt/venv` `/opt/venv`
`ENV` `PATH="/opt/venv/bin:$PATH"`

`VOLUME` `/data`

`CMD` [`"gunicorn"`, `"--bind"`, `"0.0.0.0:80"`, `"ex_serv:app"` ]`

结论

事实证明,Alpine 镜像体积(远小于其他镜像)最小。然而,它们比基础的 Debian 镜像小得多。除非你的功能在 Alpine Linux 上无法运行,否则 Alpine 应该是你的基础镜像。

此外,我建议在 Dockerfile 中定义一个两阶段的构建和发布流程。在这个例子中,镜像大小的差异并不显著,但随着你向镜像添加更多依赖项,镜像体积会迅速增大。例如,当我们包含库的 *-dev 包和整个 clang 工具链时,镜像体积会迅速增长。

相关推荐
布史2 小时前
Linux软链接应用详解:从原理到实战案例
linux·运维·服务器
顶点多余2 小时前
linux的基本指令
linux·运维·服务器
Peterrrr09112 小时前
深入理解 Shell 编程:正则表达式与 sed 文本处理器
linux·运维·正则表达式·sed·linux命令
海清河晏1112 小时前
Linux进阶篇:网络编程
linux·运维·网络
2301_811958382 小时前
服务器自己账号下安装conda
linux·python·conda
whltaoin2 小时前
25年12月26日-福州某科技公司一面面试原题
java·linux·docker·面试·职场和发展·k8s·springboot
网硕互联的小客服2 小时前
如何搭建个人邮局或者企业邮局?使用什么邮局系统好?
linux·运维·服务器·安全
九皇叔叔2 小时前
CentOS 容器安装部署
linux·运维·centos
蓝影铁哥2 小时前
浅谈5款Java微服务开发框架
java·linux·运维·开发语言·数据库·微服务·架构