大家好!我是大聪明-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 工具链时,镜像体积会迅速增长。