《Docker技术入门与实战 第4版》阅读笔记 6 使用 Dockerfile 创建镜像 2
8.3 创建镜像
在 Dockerfile 编写完毕之后,读者可以使用 docker image build 命令来创建镜像,基本格式为:
bash
docker build [OPTIONS] PATH | URL | -
此命令将读取指定路径(包括子目录)下的 Dockerfile,并将该路径下的所有数据作为上下文 (Context) 发送给 Docker 服务端。Docker 服务端在验证 Dockerfile 格式无误后,会逐条执行其中定义的指令。当遇到 ADD、COPY 和 RUN 指令时,会生成一层新的镜像。最终,若镜像创建成功,系统将返回最终镜像的 ID。
上下文路径指 Dockerfile 文件所在的路径
请注意,如果上下文过大,将导致向服务端发送大量数据,从而延长创建过程。因此,除非文件是生成镜像所必需的,否则请勿将其放在上下文路径下。如果需要使用非上下文路径下的 Dockerfile,则可以通过 -f 选项来指定其路径。要为生成的镜像指定标签信息,可以使用 -t 选项。可以多次使用此选项,以一次性为镜像添加多个名称。
例如,若上下文路径为 /tmp/docker_builder/,且希望生成的镜像标签为 builder/first_image:1.0.0,则可以使用以下命令:
bash
docker build -t builder/first_image:1.0.0 /tmp/docker_builder/
8.3.1 命令选项
docker image build 命令支持很多参数,可以调整创建镜像的过程。选项介绍如表所示。
🏷️ 镜像命名与元数据
| 参数 | 说明 |
|---|---|
-t, --tag |
为构建的镜像指定名称和标签(格式:名称:标签),可多次使用 |
--label |
为生成的镜像设置元数据标签 |
--iidfile |
将构建出的镜像 ID 写入到指定文件中 |
📂 构建上下文与 Dockerfile
| 参数 | 说明 |
|---|---|
-f, --file |
指定 Dockerfile 的路径(默认是上下文目录下的 Dockerfile) |
--compress |
使用 gzip 压缩构建上下文后再发送给守护进程 |
⚙️ 构建行为控制
| 参数 | 说明 |
|---|---|
--build-arg |
设置构建时的变量(对应 Dockerfile 中的 ARG 指令) |
--no-cache |
完全禁用缓存,强制重新执行所有构建步骤 |
--pull |
始终尝试拉取基础镜像的最新版本 |
--target |
指定多阶段构建(Multi-stage build)中的目标构建阶段 |
--squash |
将新构建的所有层压缩成一个单独的层 |
--platform |
如果服务器支持多平台,可指定构建的目标平台(如 linux/amd64) |
🗑️ 中间容器清理
| 参数 | 说明 |
|---|---|
--rm |
构建成功后删除中间容器(默认开启) |
--force-rm |
无论构建成功与否,都强制删除中间容器 |
📊 输出与进度
| 参数 | 说明 |
|---|---|
-q, --quiet |
安静模式,仅输出镜像 ID 和错误信息 |
--progress |
设置构建进度的输出模式(auto, plain, tty) |
🔒 安全与网络
| 参数 | 说明 |
|---|---|
--network |
设置构建过程中 RUN 指令使用的网络模式(如 host, none) |
--add-host |
在构建期间的 /etc/hosts 中添加自定义的主机名到 IP 映射 |
--disable-content-trust |
跳过镜像签名验证(默认开启跳过) |
--secret |
向构建过程暴露一个密钥文件(需开启 BuildKit) |
--ssh |
向构建过程暴露 SSH 代理套接字或密钥(需开启 BuildKit) |
💻 资源限制
| 参数 | 说明 |
|---|---|
-m, --memory |
限制构建容器的内存使用量 |
--memory-swap |
限制内存加交换分区(Swap)的总量 |
--cpu-shares |
设置 CPU 使用的相对权重 |
--cpu-period |
限制 CPU CFS(完全公平调度器)的周期 |
--cpu-quota |
限制 CPU CFS 的配额 |
--cpuset-cpus |
限制允许使用的 CPU 核心(例如 0-3, 0,1) |
--cpuset-mems |
限制允许使用的内存节点 |
--shm-size |
设置 /dev/shm 的大小(默认 64MB) |
--ulimit |
设置 Ulimit 选项 |
--cgroup-parent |
为构建容器指定可选的父 cgroup |
--isolation |
指定容器的隔离技术 |
🚀 高级构建功能 (BuildKit)
| 参数 | 说明 |
|---|---|
--cache-from |
指定外部缓存源(如注册表镜像、本地文件、GitHub Actions 缓存等) |
--cache-to |
将构建缓存导出到外部目标(如注册表、本地文件、S3 等) |
-o, --output |
指定构建结果的输出目的地(例如直接导出到本地目录) |
--build-context |
定义额外的构建上下文(可以是本地路径、容器镜像、Git URL 等) |
8.3.2 选择父镜像
通常生成新的镜像时都需要通过 FROM 指令来指定父镜像。父镜像是所生成镜像的基础,会直接影响镜像的大小和功能。
用户可以选择两种镜像作为父镜像,一种是基础镜像 (baseimage),另一种是普通镜像(基于基础镜像创建的镜像)。
基础镜像比较特殊,它的 Dockerfile 中往往不存在 FROM 指令,或者基于 scratch 镜像 (FROM scratch),这意味着基础镜像在整个镜像树中处于根的位置。
下面的 Dockerfile 定义了一个简单的基础镜像,将用户提前编译好的二进制可执行文件 binary 复制到镜像中,运行容器时执行 binary 命令:
bash
FROM scratch
ADD binary /
CMD ["/binary"]
普通镜像也可以作为父镜像来使用,包括常见的 busybox、debian、ubuntu 等。
8.3.3 使用 .dockerignore 文件
可以通过 .dockerignore 文件(每一行添加一条匹配规则)来让 Docker 忽略匹配路径或文件,避免在创建镜像时将无关数据发送到服务端。例如,下面的示例中包括了 6 行忽略的模式(第一行为注释):
bash
# 1. 基础通配符 (*):匹配当前目录层级下的零个或多个任意字符
# 忽略根目录下所有的 .log 文件
*.log
# 2. 单字符通配符 (?):匹配恰好一个任意字符
# 忽略 file1.txt、fileA.txt,但不忽略 file.txt 或 file10.txt
file?.txt
# 3. 递归匹配 (**):匹配任意层级的目录结构
# 忽略项目中任意深度的 temp 文件或目录(如 src/temp、src/utils/temp)
**/temp
# 4. 根目录限定 (/):只匹配构建上下文根目录中的文件
# 仅忽略根目录下的 README.md,docs/README.md 不会被忽略
/README.md
# 5. 目录限定 (/ 结尾):只匹配目录,不匹配同名普通文件
# 忽略根目录下的 logs 文件夹及其所有内容,但不会忽略名为 logs 的普通文件
logs/
# 6. 例外规则 (!):取反,在前面的忽略规则中恢复特定的文件
# 虽然上面忽略了 *.log,但这里将 important.log 重新包含在构建上下文中
!important.log
.dockerignore 文件中的规则语法支持 Golang 风格的路径匹配规则,其中,"*" 表示任意多个字符;"?" 代表单个字符;"!" 表示不匹配(即不忽略指定的目录或文件)。
8.3.4 多步骤创建
Docker 支持多步骤镜像创建特性,可以精简最终生成的镜像大小。对于需要编译的应用(如C、Go或Java语言等),通常情况下至少需要准备两个环境的 Docker 镜像:
- 编译环境镜像:包括完整的编译引擎、依赖库等,往往比较庞大,作用是将应用编译为二进制文件。
- 运行环境镜像:利用编译好的二进制文件,运行应用,由于不需要编译环境,因此体积比较小。
使用多步骤创建,可以在保证最终生成的运行环境镜像保持精简的情况下,使用单一的 Dockerfile,降低维护复杂度。
8.4 最佳实践
最佳实践的目标是根据需求来定制适合自己并且高效、便捷的镜像。
首先,读者要深入了解每个指令的含义和执行效果,亲自编写一些简单的示例进行测试,确保充分理解后再编写正式的 Dockerfile。此外,Docker Hub 官方仓库中提供了大量优秀的镜像和对应的 Dockerfile,读者可以通过阅读它们来学习如何编写高效的 Dockerfile。
下面是一些构建镜像的经验。读者在生成镜像的过程中可以参考借鉴,以完善所生成的镜像。
- 精简镜像用途:尽量让每个镜像的用途都比较集中、单一,避免构造庞大而复杂的多功能镜像
- 选择合适的基础镜像:容器的核心是应用。选择过大的父镜像(如 Ubuntu 系统镜像)会造成最终生成应用镜像的臃肿,通常建议选用精简的应用镜像(如 node:slim),或者较为轻量级的系统镜像(如 alpine、busybox 或 debian)。
- 提供注释和维护者信息:Dockerfile 也是一种代码,需要考虑方便后续扩展和他人使用。
- 正确使用版本号:使用明确的版本号信息,如 减少镜像层数:为了生成尽量少的镜像层数,可以尝试合并RUN、ADD和COPY指令。通常情况下,多条RUN指令可以合并为一条RUN指令。1.0、2.0,而非依赖于默认的latest。通过版本号可以避免环境不一致导致的问题。
- 减少镜像层数:为了生成尽量少的镜像层数,可以尝试合并 RUN、ADD 和 COPY 指令。通常情况下,多条 RUN 指令可以合并为一条 RUN 指令。
- 恰当使用多步骤创建(17.05+版本支持):通过多步骤创建,可以将编译和运行过程分开,确保最终生成的镜像仅包含运行应用所需的最小化环境。当然,读者也可以分别构建编译镜像和运行镜像来达到类似的效果,但这种方式需要维护多个 Dockerfile。
- 使用 .dockerignore 文件:利用该文件可以标记在执行 docker build 时忽略的路径和文件,避免发送不必要的数据内容,从而加快整个镜像的创建过程。
- 及时删除临时文件和缓存文件:例如在执行 apt-get 指令后,/var/cache/apt 目录下可能会缓存一些安装包,需要及时清理。
- 提高生成速度:例如,合理使用缓存、减少内容目录下的文件,或使用 .dockerignore 文件指定等,以加快镜像创建速度。
- 调整合理的指令顺序:在开启缓存的情况下,内容不变的指令尽量放在前面,以便最大限度地复用缓存。
- 减少外部源的干扰:如果确实需要从外部引入数据,则务必指定持久且带版本信息的地址,以便其他人能够重复使用而不会出错。
遵循这些最佳实践,读者将能够创建出高效、易于维护和使用的Docker镜像,从而优化容器部署和应用管理过程。
8.5 本章小结
编写一个高质量的 Dockerfile 并非易事,这需要不断地进行学习和实践。在本书的第二部分,笔者精选了多个具有代表性的 Dockerfile 实例,帮助读者更好地理解镜像构建原理并参考实例进行实践。