[Buildkit] Docker buildx 构建 - 图1Docker是一种流行的部署工具,使我们能够打包和运行应用程序。由于采用率很高,需要根据不同的要求扩展功能。因此,为了实现这一目标,第三方使用了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 版本:

  1. $ docker --version
  2. Docker version 19.03.8, build afacb8b

接下来,我们通过设置环境变量来启用Docker实验功能:

  1. $ export DOCKER_CLI_EXPERIMENTAL=enabled

为了确保我们的设置在会话后保留,我们将变量添加到Bash的$HOME/.bashrc中。完成后,我们现在应该可以访问buildx 了:

  1. $ docker buildx
  2. Usage: docker buildx COMMAND
  3. Build with BuildKit
  4. Management Commands:
  5. imagetools Commands to work on images in registry
  6. Commands:
  7. bake Build from a file
  8. build Start a build
  9. create Create a new builder instance
  10. inspect Inspect current builder instance
  11. ls List builder instances
  12. rm Remove a builder instance
  13. stop Stop builder instance
  14. use Set the current builder instance
  15. version Show buildx version information
  16. Run 'docker buildx COMMAND --help' for more information on a command.

这显示了常用命令以及每个命令的语法。

使用 buildx 进行构建的方法如下:

  1. 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。

创建实例:

  1. # 创建一个名为 "multi-platform-builder" 的 Buildx 构建器,并启用它,以便支持多个平台的容器镜像构建
  2. $ sudo docker buildx create --use --platform=linux/arm64,linux/amd64 --name multi-platform-builder
  3. multi-platform-builder
  4. # 检查和验证 Docker Buildx 构建器的命令,以确保它已正确设置和准备好进行构建任务
  5. $ sudo docker buildx inspect --bootstrap
  6. [+] Building 15.2s (1/1) FINISHED
  7. => [internal] booting buildkit
  8. => => pulling image moby/buildkit:buildx-stable-1
  9. => => creating container buildx_buildkit_multi-platform-builder0
  10. Name: multi-platform-builder
  11. Driver: docker-container
  12. Nodes:
  13. Name: multi-platform-builder0
  14. Endpoint: unix:///var/run/docker.sock
  15. Status: running
  16. 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 内容如下:

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("hello world")
  5. }

定义构建过程的 Dockerfile 如下:

  1. FROM golang:1.17 as builder
  2. WORKDIR /opt/app
  3. COPY . .
  4. RUN go mod init main && go build -o example
  5. FROM alpine:latest
  6. WORKDIR /opt/app
  7. COPY --from=builder /opt/app/example ./example

构建过程分为两个阶段:

  • 在一阶段中,我们将拉取一个 golang 镜像,并使用 Go 其编译为二进制文件。
  • 然后拉取目标平台的 alpine 镜像,并将上一阶段的编译结果拷贝到镜像中。

2.2 执行跨平台构建

  1. $ docker buildx build -f ./Dockerfile-4 --platform linux/amd64,linux/arm64 -t fly190712/example:v2 --push .
  2. [+] Building 80.6s (27/27) FINISHED
  3. => [internal] load build definition from Dockerfile-4 0.0s
  4. => => transferring dockerfile: 337B 0.0s
  5. => resolve image config for docker.io/docker/dockerfile:1 10.5s
  6. => [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s
  7. => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:ac85f380a63b13dfcefa89046420e1781752bab2021 0.0s
  8. => => resolve docker.io/docker/dockerfile:1@sha256:ac85f380a63b13dfcefa89046420e1781752bab202122f8f50032e 0.0s
  9. => [linux/amd64 internal] load metadata for docker.io/library/golang:1.17 2.6s
  10. => [linux/arm64 internal] load metadata for docker.io/library/alpine:latest 5.4s
  11. => [linux/amd64 internal] load metadata for docker.io/library/alpine:latest 5.1s
  12. => [linux/arm64 internal] load metadata for docker.io/library/golang:1.17 4.5s
  13. => [auth] library/alpine:pull token for registry-1.docker.io 0.0s
  14. => [internal] load .dockerignore 0.0s
  15. => => transferring context: 2B 0.0s
  16. => [linux/arm64 builder 1/4] FROM docker.io/library/golang:1.17@sha256:87262e4a4c7db56158a80a18fefdc4fee5 0.0s
  17. ...
  18. => [linux/amd64 stage-1 1/3] FROM docker.io/library/alpine:latest@sha256:7144f7bab3d4c2648d7e59409f15ec52 0.0s
  19. ...
  20. => [internal] load build context 0.0s
  21. => => transferring context: 1.32kB 0.0s
  22. => [linux/amd64 builder 1/4] FROM docker.io/library/golang:1.17@sha256:87262e4a4c7db56158a80a18fefdc4fee5 0.0s
  23. ... 0.0s
  24. => [linux/amd64 builder 4/4] RUN go mod init main && go build -o example 0.7s
  25. => [linux/arm64 builder 4/4] RUN go mod init main && go build -o example 3.1s
  26. => exporting to image 13.0s
  27. 10.1s
  28. => => pushing manifest for docker.io/fly190712/example:v2@sha256:507132b53d6c71d36dcedb916c18ceee9a742059 2.7s
  29. => [auth] fly190712/example:pull,push token for registry-1.docker.io 0.0s

注意:构建完成后,镜像在docker images中是查不到的,因为这个镜像仅位于 buildx 缓存中。我们这里使用—push标志可用于直接将镜像推送到 dockerhub。

[Buildkit] Docker buildx 构建 - 图2

多架构镜像

除此之外,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 命令:

  1. $ docker buildx build -f ./Dockerfile-4 --platform linux/amd64,linux/arm64 -t fly190712/example:v2 -o type=registry .

该命令将在当前目录同时构建 linux/amd64、 linux/arm64 和 linux/arm 三种平台的镜像,并将输出结果直接推送到远程的阿里云镜像仓库中。

构建过程可拆解如下:

  1. docker 将构建上下文传输给 builder 实例。
  2. builder 为命令行 —platform 选项指定的每一个目标平台构建镜像,包括拉取基础镜像和执行构建步骤。
  3. 导出构建结果,镜像文件层被推送到远程仓库。
  4. 生成一个清单 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) 以及较旧版本的镜像仓库,在构建跨平台镜像时可能会遇到构建成功但推送失败的问题,报错如下:

  1. => => pushing layers 12.3s
  2. => => pushing manifest for registry.xxx.com/xxx/postgres-vb 0.9s
  3. => [auth] xxx/postgres-vb:pull,push token for registry.xxx.com 0.0s
  4. => ERROR merging manifest list registry.xxx.com/xxx/postgres-vb:v1.2.0

这是因为新版本默认开启了 **Build attestations** 功能以增强供应链安全,但同时带来了兼容性问题。如果你并不需要,可添加如下 build 选项禁用它以解决该问题:

  1. docker buildx build --sbom=false --provenance=false

3.3 buildx 的跨平台构建策略

根据构建节点和目标程序语言不同,buildx** 支持以下三种跨平台构建策略**:

  1. 通过 QEMU 的用户态模式创建轻量级的虚拟机,在虚拟机系统中构建镜像。
  2. 一个 builder 实例中加入多个不同目标平台的节点,通过原生节点构建对应平台镜像。
  3. 分阶段构建并且交叉编译到不同的目标架构

QEMU 通常用于模拟完整的操作系统,它还可以通过用户态模式运行:以 binfmt_misc 在宿主机系统中注册一个二进制转换处理程序,并在程序运行时动态翻译二进制文件,根据需要将系统调用从目标 CPU 架构转换为当前系统的 CPU 架构。最终的效果就像在一个虚拟机中运行目标 CPU 架构的二进制文件。

Docker Desktop 内置了 QEMU 支持,其他满足运行要求的平台可通过以下方式安装

  1. $ docker run --privileged --rm tonistiigi/binfmt --install all

这种方式不需要对已有的 Dockerfile 做任何修改,实现的成本很低,但显而易见效率并不高。

将不同系统架构的原生节点添加到 builder 实例中可以为跨平台编译带来更好的支持,而且效率更高,但需要有足够的基础设施支持。

如果构建项目所使用的程序语言支持交叉编译(如 C 和 Go),可以利用 Dockerfile 提供的分阶段构建特性:首先在和构建节点相同的架构中编译出目标架构的二进制文件,再将这些二进制文件复制到目标架构的另一镜像中。