Docker是一种流行的部署工具,使我们能够打包和运行应用程序。由于采用率很高,需要根据不同的要求扩展功能。因此,为了实现这一目标,第三方使用了docker 插件。
例如,如果我们希望数据能够跨不同主机保存数据,我们可以使用卷插件。另一个常用的插件是Docker buildx。它通过使用 BuildKit 构建器扩展了镜像的构建能力。因此,通过该插件,我们可以为不同的平台和架构构建镜像。此外,它还支持具有自定义上下文的并行多阶段构建。
1 Docker buildx
默认的 **docker build 命令无法完成跨平台构建任务,我们需要为 docker 命令行安装 buildx** 插件扩展其功能。buildx 能够使用由 Moby BuildKit 提供的构建镜像额外特性,它能够创建多个 builder 实例,在多个节点并行地执行构建任务,以及跨平台构建。
1.1 启用 Buildx
首先,为了运行buildx ,我们需要安装 Docker。Docker buildx支持从Docker engine19.00 开始提供。
首先检查我们的 Docker 版本:
$ docker --version
Docker version 19.03.8, build afacb8b
接下来,我们通过设置环境变量来启用Docker实验功能:
$ export DOCKER_CLI_EXPERIMENTAL=enabled
为了确保我们的设置在会话后保留,我们将变量添加到Bash的$HOME/.bashrc中。完成后,我们现在应该可以访问buildx 了:
$ docker buildx
Usage: docker buildx COMMAND
Build with BuildKit
Management Commands:
imagetools Commands to work on images in registry
Commands:
bake Build from a file
build Start a build
create Create a new builder instance
inspect Inspect current builder instance
ls List builder instances
rm Remove a builder instance
stop Stop builder instance
use Set the current builder instance
version Show buildx version information
Run 'docker buildx COMMAND --help' for more information on a command.
这显示了常用命令以及每个命令的语法。
使用 buildx 进行构建的方法如下:
docker buildx build .
buildx 和 docker build 命令的使用体验基本一致,还支持 build 常用的选项如 -t、-f等。
1.2 新建 builder 实例
docker buildx 通过 builder 实例对象来管理构建配置和节点,命令行将构建任务发送至 builder 实例,再由 builder 指派给符合条件的节点执行。我们可以基于同一个 **docker** 服务程序创建多个 builder 实例,提供给不同的项目使用以隔离各个项目的配置,也可以为一组远程 docker 节点创建一个 builder 实例组成构建阵列,并在不同阵列之间快速切换。
使用 **docker buildx create** 命令可以创建 builder 实例,这将以当前使用的 docker 服务为节点创建一个新的 builder 实例。要使用一个远程节点,可以在创建示例时通过 DOCKER_HOST 环境变量指定远程端口或提前切换到远程节点的 docker context。
创建实例:
# 创建一个名为 "multi-platform-builder" 的 Buildx 构建器,并启用它,以便支持多个平台的容器镜像构建
$ sudo docker buildx create --use --platform=linux/arm64,linux/amd64 --name multi-platform-builder
multi-platform-builder
# 检查和验证 Docker Buildx 构建器的命令,以确保它已正确设置和准备好进行构建任务
$ sudo docker buildx inspect --bootstrap
[+] Building 15.2s (1/1) FINISHED
=> [internal] booting buildkit
=> => pulling image moby/buildkit:buildx-stable-1
=> => creating container buildx_buildkit_multi-platform-builder0
Name: multi-platform-builder
Driver: docker-container
Nodes:
Name: multi-platform-builder0
Endpoint: unix:///var/run/docker.sock
Status: running
Platforms: linux/arm64*, linux/amd64*, linux/amd64/v2, linux/amd64/v3, linux/amd64/v4, linux/386
2 使用buildx构建多架构镜像
buildx可以执行所有Docker构建功能。因此,我们可以轻松地运行并执行它们。例如,指定目标平台、构建缓存和输出配置。除此之外, buildx还提供了额外的功能。
首先,能够同时为多个平台构建镜像。其次,能够在构建过程中自定义输入、参数或变量。
2.1 源代码和 Dockerfile
下面将以一个简单的 Go 项目作为示例,假设示例程序文件 main.go 内容如下:
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
定义构建过程的 Dockerfile 如下:
FROM golang:1.17 as builder
WORKDIR /opt/app
COPY . .
RUN go mod init main && go build -o example
FROM alpine:latest
WORKDIR /opt/app
COPY --from=builder /opt/app/example ./example
构建过程分为两个阶段:
- 在一阶段中,我们将拉取一个 golang 镜像,并使用 Go 其编译为二进制文件。
- 然后拉取目标平台的 alpine 镜像,并将上一阶段的编译结果拷贝到镜像中。
2.2 执行跨平台构建
$ docker buildx build -f ./Dockerfile-4 --platform linux/amd64,linux/arm64 -t fly190712/example:v2 --push .
[+] Building 80.6s (27/27) FINISHED
=> [internal] load build definition from Dockerfile-4 0.0s
=> => transferring dockerfile: 337B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1 10.5s
=> [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:ac85f380a63b13dfcefa89046420e1781752bab2021 0.0s
=> => resolve docker.io/docker/dockerfile:1@sha256:ac85f380a63b13dfcefa89046420e1781752bab202122f8f50032e 0.0s
=> [linux/amd64 internal] load metadata for docker.io/library/golang:1.17 2.6s
=> [linux/arm64 internal] load metadata for docker.io/library/alpine:latest 5.4s
=> [linux/amd64 internal] load metadata for docker.io/library/alpine:latest 5.1s
=> [linux/arm64 internal] load metadata for docker.io/library/golang:1.17 4.5s
=> [auth] library/alpine:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [linux/arm64 builder 1/4] FROM docker.io/library/golang:1.17@sha256:87262e4a4c7db56158a80a18fefdc4fee5 0.0s
...
=> [linux/amd64 stage-1 1/3] FROM docker.io/library/alpine:latest@sha256:7144f7bab3d4c2648d7e59409f15ec52 0.0s
...
=> [internal] load build context 0.0s
=> => transferring context: 1.32kB 0.0s
=> [linux/amd64 builder 1/4] FROM docker.io/library/golang:1.17@sha256:87262e4a4c7db56158a80a18fefdc4fee5 0.0s
... 0.0s
=> [linux/amd64 builder 4/4] RUN go mod init main && go build -o example 0.7s
=> [linux/arm64 builder 4/4] RUN go mod init main && go build -o example 3.1s
=> exporting to image 13.0s
10.1s
=> => pushing manifest for docker.io/fly190712/example:v2@sha256:507132b53d6c71d36dcedb916c18ceee9a742059 2.7s
=> [auth] fly190712/example:pull,push token for registry-1.docker.io 0.0s
注意:构建完成后,镜像在docker images中是查不到的,因为这个镜像仅位于 buildx 缓存中。我们这里使用—push标志可用于直接将镜像推送到 dockerhub。
多架构镜像
除此之外,docker buildx build 支持丰富的输出行为,通过—output=[PATH,-,type=TYPE[,KEY=VALUE] 选项可以指定构建结果的输出类型和路径等,常用的输出类型有以下几种:
- local:构建结果将以文件系统格式写入 dest 指定的本地路径, 如 —output type=local,dest=./output。
- tar:构建结果将在打包后写入 dest 指定的本地路径。
- oci:构建结果以 OCI 标准镜像格式写入 dest 指定的本地路径。
- docker:构建结果以 Docker 标准镜像格式写入 dest 指定的本地路径或加载到 docker 的镜像库中。同时指定多个目标平台时无法使用该选项。
- image:以镜像或者镜像列表输出,并支持 push=true 选项直接推送到远程仓库,同时指定多个目标平台时可使用该选项。
- registry:type=image,push=true 的精简表示。
我们执行如下 docker buildx build 命令:
$ docker buildx build -f ./Dockerfile-4 --platform linux/amd64,linux/arm64 -t fly190712/example:v2 -o type=registry .
该命令将在当前目录同时构建 linux/amd64、 linux/arm64 和 linux/arm 三种平台的镜像,并将输出结果直接推送到远程的阿里云镜像仓库中。
构建过程可拆解如下:
- docker 将构建上下文传输给 builder 实例。
- builder 为命令行 —platform 选项指定的每一个目标平台构建镜像,包括拉取基础镜像和执行构建步骤。
- 导出构建结果,镜像文件层被推送到远程仓库。
- 生成一个清单 JSON 文件,并将其作为镜像标签推送给远程仓库。
3 更重要的补充
3.1 构建驱动
buildx 实例通过两种方式来执行构建任务,两种执行方式被称为使用不同的「驱动」:
- docker** 驱动**:使用 Docker 服务程序中集成的 BuildKit 库执行构建。
- docker-container** 驱动**:启动一个包含 BuildKit 的容器并在容器中执行构建。
docker** 驱动无法使用一小部分 buildx 的特性**(如在一次运行中同时构建多个平台镜像),此外在镜像的默认输出格式上也有所区别:docker 驱动默认将构建结果以 Docker 镜像格式直接输出到 docker 的镜像目录(通常是 /var/lib/overlay2),之后执行 docker images 命令可以列出所输出的镜像;
而 docker container** 则需要通过 —output 选项指定输出格式为镜像或其他格式**。
为了一次性构建多个平台的镜像,本文使用 docker container 作为默认的 builder 实例驱动。
3.2 解决 ERROR merging manifest list
如果你使用了较新版本的 buildx(>=0.10) 和 buildkit(>=0.11) 以及较旧版本的镜像仓库,在构建跨平台镜像时可能会遇到构建成功但推送失败的问题,报错如下:
=> => pushing layers 12.3s
=> => pushing manifest for registry.xxx.com/xxx/postgres-vb 0.9s
=> [auth] xxx/postgres-vb:pull,push token for registry.xxx.com 0.0s
=> ERROR merging manifest list registry.xxx.com/xxx/postgres-vb:v1.2.0
这是因为新版本默认开启了 **Build attestations** 功能以增强供应链安全,但同时带来了兼容性问题。如果你并不需要,可添加如下 build 选项禁用它以解决该问题:
docker buildx build --sbom=false --provenance=false
3.3 buildx 的跨平台构建策略
根据构建节点和目标程序语言不同,buildx** 支持以下三种跨平台构建策略**:
- 通过 QEMU 的用户态模式创建轻量级的虚拟机,在虚拟机系统中构建镜像。
- 在一个 builder 实例中加入多个不同目标平台的节点,通过原生节点构建对应平台镜像。
- 分阶段构建并且交叉编译到不同的目标架构。
QEMU 通常用于模拟完整的操作系统,它还可以通过用户态模式运行:以 binfmt_misc 在宿主机系统中注册一个二进制转换处理程序,并在程序运行时动态翻译二进制文件,根据需要将系统调用从目标 CPU 架构转换为当前系统的 CPU 架构。最终的效果就像在一个虚拟机中运行目标 CPU 架构的二进制文件。
Docker Desktop 内置了 QEMU 支持,其他满足运行要求的平台可通过以下方式安装:
$ docker run --privileged --rm tonistiigi/binfmt --install all
这种方式不需要对已有的 Dockerfile 做任何修改,实现的成本很低,但显而易见效率并不高。
将不同系统架构的原生节点添加到 builder 实例中可以为跨平台编译带来更好的支持,而且效率更高,但需要有足够的基础设施支持。
如果构建项目所使用的程序语言支持交叉编译(如 C 和 Go),可以利用 Dockerfile 提供的分阶段构建特性:首先在和构建节点相同的架构中编译出目标架构的二进制文件,再将这些二进制文件复制到目标架构的另一镜像中。