原文:https://www.docker.com/blog/new-dockerfile-capabilities-v1-7-0/
Dockerfile 是使用 Docker 的相关开发人员的基本工具,用来充当构建 Docker 镜像的模板,在这个文件中包含用户可以在命令行上调用来构建镜像的所有命令。了解并有效利用 Dockerfile 可以显着简化开发流程,实现镜像创建的自动化并确保不同开发阶段的环境一致。Dockerfile 对于定义 Docker 容器内的项目环境、依赖项和应用程序配置至关重要。
借助新版本的 BuildKit 构建器工具包、Docker Buildx CLI 和 BuildKit v1.7.0 版本的 Dockerfile 前端,开发人员现在可以访问增强的 Dockerfile 功能。本文我们将深入探讨这些新的 Dockerfile 功能,并解释如何在项目中利用它们来进一步优化 Docker 工作流程。
一、版本控制
在开始之前,我们先快速提醒一下 Dockerfile 的版本控制方式以及您应该执行哪些操作来更新它。
尽管大多数项目使用 Dockerfile 来构建镜像,但其实 BuildKit 不仅限于该格式。BuildKit 支持多个不同的前端来定义 BuildKit 要处理的构建步骤。任何人都可以创建这些前端,将它们打包为常规容器镜像,并在调用构建时从注册表加载它们。
在新版本中,我们向 Docker Hub 发布了两个此类镜像:docker/dockerfile:1.7.0 和 docker/dockerfile:1.7.0-labs。
要使用新特性,您需要在文件开头指定 #syntax 指令,以告诉 BuildKit 用于构建的前端镜像。这里我们将其设置为使用最新的 1.x.x 主要版本,例如:
# syntax=docker/dockerfile:1.x.x
FROM alpine
这意味着 BuildKit 与 Dockerfile 前端语法解耦,您可以立即开始使用新的 Dockerfile 特性,而不必担心您正在使用哪个 BuildKit 版本。只要您在 Dockerfile 顶部定义正确的 #syntax 指令,本文中描述的所有示例都适用于任何支持 BuildKit 的 Docker 版本。
关于 Dockerfile 前端版本的更多信息,请参阅 https://docs.docker.com/build/dockerfile/frontend/ 以了解更多。
二、变量扩展
编写 Dockerfile 时,构建步骤可以包含使用构建参数 (ARG) 和环境变量 (ENV) 指令定义的变量。构建参数和环境变量之间的区别在于,环境变量保留在生成的镜像中,并在从中创建容器时持续存在。
当您使用此类变量时,您很可能在 COPY、RUN 和其他命令中使用 ${NAME},或者 $NAME。
您可能不知道 Dockerfile 支持两种形式的类似 Bash 的变量扩展:
-
${variable:-word}:如果变量未设置,则将值设置为 word
-
${variable:+word}:如果变量已设置,则将值设置为 word
到目前为止,这些特殊形式在 Dockerfile 中并没有多大用处,因为 ARG 指令的默认值可以直接设置:
FROM alpine
ARG foo="default value"
如果您是各种 shell 专家,您就会知道 Bash 和其他工具通常具有许多附加形式的变量扩展,以简化脚本的开发。
在 Dockerfile v1.7 中,我们就添加了一部分这样的功能。现在,您可以在 Dockerfile 中使用以下形式的变量扩展:
-
${variable#pattern} 和 ${variable##pattern} 从变量值中删除最短或最长的前缀
-
${variable%pattern} 和 ${variable%%pattern} 从变量值中删除最短或最长的后缀
-
${variable/pattern/replacement} 替换最先出现的 pattern 模式
-
${variable//pattern/replacement} 替换所有出现的 pattern 模式
这些规则的使用方式一开始可能并不完全明显。让我们看一下实际 Dockerfile 中的一些示例。例如,项目通常无法就下载依赖项的版本是否应具有 v 前缀达成一致,下面的方式可以允许您获取所需的格式:
# example VERSION=1.2.3
ARG VERSION=${VERSION#v}
# VERSION is now 1.2.3
又比如下面的这个示例中同一个 VERSION 变量我们可以在不同的地方多次使用:
ARG VERSION=v1.7.13
ADD https://github.com/containerd/containerd/releases/download/${VERSION}/containerd-${VERSION#v}-linux-amd64.tar.gz /
有的时候我们可能希望不同平台的构建配置不同的命令行,这时候我们就可以使用 BuildKit 提供的内置变量,例如 TARGETOS 和 TARGETARCH。但是需要注意并非所有项目都使用相同的值,例如,在容器和 Go 生态中,我们将 64 位 ARM 架构称为 arm64,但有时您需要 aarch64,这个时候我们就可以使用 ${variable/pattern/replacement} 来进行替换:
ADD https://github.com/oven-sh/bun/releases/download/bun-v1.0.30/bun-linux-${TARGETARCH/arm64/aarch64}.zip /
接下来让我们看看新的扩展如何在多阶段构建中发挥作用。
如果您构建多平台镜像并希望仅针对特定平台运行其他 COPY 或 RUN 命令,则可以使用该模式。简而言之,其想法是定义一个全局构建参数,然后定义构建阶段,在阶段名称中使用构建参数值,同时通过构建参数名称指向目标阶段的基础。
以前的方式如下所示:
ARG BUILD_VERSION=1
FROM alpine AS base
RUN …
FROM base AS branch-version-1
RUN touch version1
FROM base AS branch-version-2
RUN touch version2
FROM branch-version-${BUILD_VERSION} AS after-condition
FROM after-condition
RUN …
使用此模式进行多平台构建时,限制之一是 build-arg 的所有可能值都需要由 Dockerfile 定义。因为我们希望 Dockerfile 的构建方式可以在任何平台上构建,而不是将其限制在特定的平台上,所以这种方式会有一些限制。
通过新的扩展,我们可以来演示仅在 RISC-V 上运行特殊命令,这仍然有些新,可能需要自定义行为:
#syntax=docker/dockerfile:1.7
ARG ARCH=${TARGETARCH#riscv64}
ARG ARCH=${ARCH:+"common"}
ARG ARCH=${ARCH:-$TARGETARCH}
FROM --platform=$BUILDPLATFORM alpine AS base-common
ARG TARGETARCH
RUN echo "Common build, I am $TARGETARCH" > /out
FROM --platform=$BUILDPLATFORM alpine AS base-riscv64
ARG TARGETARCH
RUN echo "Riscv only special build, I am $TARGETARCH" > /out
FROM base-${ARCH} AS base
我们再仔细看下上面的这些 ARCH 定义:
-
第一个将 ARCH 设置为 TARGETARCH,但从该值中删除 riscv64。
-
我们实际上并不希望其他架构使用它们自己的值,而是希望它们都共享一个共同的值。因此,我们将 ARCH 设置为 common,除非它已从之前的 riscv64 规则中清除。
-
现在,如果我们仍然有一个空值,我们将其默认为 $TARGETARCH。
-
最后一个定义是可选的,因为我们已经为这两种情况提供了唯一值,但它使最终阶段名称 base-riscv64 更易于阅读。
和老的方式相比新模式不仅限于控制构建的平台差异,而且可以与任何构建参数一起使用。如果您以前使用过此模式,那么您现在可以有效地定义 else 子句,而以前,您只能使用 if 子句。
三、复制并保留父目录
以下功能已在 labs 频道中发布的,需要在 Dockerfile 顶部定义以下内容以使用此功能。
#syntax=docker/dockerfile:1.7-labs
例如,当我们在 Dockerfile 中复制文件时,可以这样做:
COPY app/file /to/dest/dir/
此示例意味着源文件直接复制到目标目录,如果源路径是一个目录,则该目录中的所有文件都将直接复制到目标路径。
如果您有如下所示的文件结构怎么办:
.
├── app1
│ ├── docs
│ │ └── manual.md
│ └── src
│ └── server.go
└── app2
└── src
└── client.go
您只想复制 app1/src 中的文件,但目标位置的最终文件将是 /to/dest/dir/app1/src/server.go 而不仅仅是 /to/dest/dir/server.go。
使用新的 COPY --parents 标志,我们可以这样来实现:
COPY --parents /app1/src/ /to/dest/dir/
这将复制 src 目录中的文件并为这些文件重新创建 app1/src 目录结构。
此外我们还可以使用通配符路径,比如我们要将两个 app 的 src 目录复制到各自的位置,可以这样实现:
COPY --parents */src/ /to/dest/dir/
这将创建 /to/dest/dir/app1 和 /to/dest/dir/app2,但不会复制 docs 目录。以前,使用单个命令无法实现这种复制。您可能需要单个文件的多个副本,或者使用 RUN --mount 指令的一些解决方法。
同样还可以使用 ** 来匹配任何目录结构下的文件,例如,要仅将 Go 源代码文件复制到构建上下文中的任何位置,可以这样实现:
COPY --parents **/*.go /to/dest/dir/
可能你会想为什么需要复制特定文件而不是直接使用 COPY ./ 来复制所有文件,记住,当您在构建中包含新文件时,构建缓存会失效。如果复制所有文件,则当添加或更改任何文件时,缓存就会失效,而如果仅复制 Go 文件,则只有这些文件中的更改会影响缓存。
新的 --parents 标志不仅适用于构建上下文中的 COPY 指令,当使用 COPY --from 在多阶段之间复制文件时,还可以在多阶段构建中使用它们。
需要注意的是,使用 COPY --from 语法,所有源路径都应该是绝对的,这意味着如果 --parents 标志与此类路径一起使用,它们将像在源阶段一样被完全复制。这可能并不总是可取的,相反,您可能想保留一些父级,但丢弃并更换其他父级。在这种情况下,可以在源路径中使用特殊的 /./ 来标记您希望复制哪些父级以及应忽略哪些父级。这个特殊的路径组件类似于 rsync 如何与 --relative 标志一起工作。
#syntax=docker/dockerfile:1.7-labs
FROM ... AS base
RUN ./generate-lot-of-files -o /out/
# /out/usr/bin/foo
# /out/usr/lib/bar.so
# /out/usr/local/bin/baz
FROM scratch
COPY --from=base --parents /out/./**/bin/ /
# /usr/bin/foo
# /usr/local/bin/baz
上面的示例显示了如何从中间阶段生成的文件集合中仅复制 bin 目录,但所有目录将保留其相对于 out 目录的路径。
四、排除过滤器
该功能特性已在 labs 频道中发布的,需要在 Dockerfile 顶部定义以下内容以使用此功能。
#syntax=docker/dockerfile:1.7-labs
使用 COPY 和 ADD 指令在 Dockerfile 中移动文件时的另一个相关场景是当您想要移动一组文件但排除特定子集时。以前,您唯一的选择是使用 RUN --mount 或尝试在 .dockerignore 文件中定义排除的文件。
然而 .dockerignore 文件并不是解决此问题的好方法,因为它们仅列出从客户端构建上下文中排除的文件,而不是从远程 Git/HTTP URL 的构建中排除的文件,并且每个 Dockerfile 仅限一个。我们可以像 .gitignore 一样使用它们来忽略不属于您的项目的文件,但不能作为定义特定于应用程序的构建逻辑的方法。
使用新的 --exclude=[pattern] 标志,现在可以直接在 Dockerfile 中为 COPY 和 ADD 命令定义此类排除过滤器。该模式使用与 .dockerignore 相同的格式。
以下示例是复制目录中除 Markdown 文件之外的所有文件:
COPY --exclude=*.md app /dest/
我们可以多次使用该标志来添加多个过滤器,比如不包括 Markdown 文件和名为 README 的文件:
COPY --exclude=*.md --exclude=README app /dest/
使用 ** 双星通配符不仅排除复制目录中的 Markdown 文件,还排除任何子目录中的 Markdown 文件:
COPY --exclude=**/*.md app /dest/
与在 .dockerignore 文件中一样,还可以使用 ! 前缀可以定义排除的例外情况。以下示例排除任何复制目录中的所有 Markdown 文件,除非该文件名为 important.md — 在这种情况下,它仍然会被复制。
COPY --exclude=**/*.md --exclude=!**/important.md app /dest/
当将 --exclude 与前面描述的 --parents 复制模式一起使用时,请注意排除模式是相对于复制的父目录。例如有以下目录结构:
assets
├── app1
│ ├── icons32x32
│ ├── icons64x64
│ ├── notes
│ └── backup
├── app2
│ └── icons32x32
└── testapp
└── icons32x32
我们可以定义如下的 Dockerfile:
COPY --parents --exclude=testapp assets/./**/icons* /dest/
该指定将创建以下目录结构,仅复制带有 icons 前缀的目录,根父目录的 assets 被跳过,此外,testapp 未被复制,因为它是使用排除过滤器定义的。
dest
├── app1
│ ├── icons32x32
│ └── icons64x64
└── app2
└── icons32x32
五、总结
本文我们介绍了 Dockerfile 的一些新功能,这些功能可以帮助您更好地控制 Dockerfile 中的构建步骤,使其更加灵活和强大。记住,即使尚未更新到最新的 Docker,您的 Dockerfile 今天也可以通过定义顶部的 #syntax 来开始使用所有这些功能。