使用 Docker 构建多架构镜像

Published: 2024-01-05

Tags: Docker

本文总阅读量

默认情况下 Docker 在机器上只能构建当前机器架构的镜像,本文实践了在 x86_64 下构建 arm64 架构的镜像,并推送多架构镜像到 Docker Hub,记录如下

环境信息

系统:Ubuntu 20.04 x86_64

Docker 版本:24.0.7

buildx 版本:0.12.0

不能使用 Centos 7 进行试验 🧪,下文有补充说明

安装 docker-buildx 插件

buildx 是 Docker CLI 的插件,它通过 BuildKit 提供了扩展的构建能力。

通过包管理器安装比较方便,但仓库中的 buildx 一般不是最新版本,更推荐手动安装最新版本。

通过二进制手动安装(推荐)

下载并放置插件,下载地址:Github buildx release

$ mkdir -p $HOME/.docker/cli-plugins

$ wget https://github.com/docker/buildx/releases/download/v0.12.0/buildx-v0.12.0.linux-amd64 -O $HOME/.docker/cli-plugins/docker-buildx
$ chmod +x $HOME/.docker/cli-plugins/docker-buildx

查看插件版本

$ docker buildx version
github.com/docker/buildx v0.12.0 542e5d810e4a1a155684f5f3c5bd7e797632a12f

通过 Linux 自带包管理器

# Ubuntu
$ sudo apt install docker-buildx-plugin 

# Centos
$ sudo yum install docker-buildx-plugin 

当前 Ubuntu 和 Centos 仓库中版本均为 v0.11.2,稍落后于最新版本 v0.12.0

了解 binfmt_misc

在继续之前,先简单了解下 binfmt_misc,仅了解即可

binfmt_misc 是 Linux 内核的一项功能,全称是混杂二进制格式的内核支持(Kernel Support for miscellaneous Binary Formats),它能够使Linux支持运行几乎任何格式的程序,包括编译后的 Java、Python 或 Emacs 程序。

为了能够让 binfmt_misc 运行任意格式的程序,至少需要做到两点:特定格式二进制程序的识别方式,以及其对应的解释器位置。虽然 binfmt_misc 听上去很强大,其实现的方式却意外地很容易理解,类似于 bash 解释器通过脚本文件的第一行(如#!/usr/bin/python3)得知该文件需要通过什么解释器运行,binfmt_misc 也预设了一系列的规则,如读取二进制文件头部特定位置的魔数,或者根据文件扩展名(如.exe、.py)以判断可执行文件的格式,随后调用对应的解释器去运行该程序。Linux默认的可执行文件格式是elf,而binfmt_misc的出现拓宽了Linux的执行限制,将一点展开成一个面,使得各种各样的二进制文件都能选择它们对应的解释器执行。

binfmt_misc 模块自 Linux 2.6.12-rc2 版本中引入,先后经历了几次功能上的略微改动,一是 3.18 版本中将解释器路径长度限制从原来的 255 字节拓宽到 1920 字节,二是在 4.8 版本中新增 “F”(fix binary,固定二进制)标志位,使 mount 命名空间变更和 chroot 后的环境中依然能够正常调用解释器执行二进制程序。由于我们需要构建多架构容器,必须使用“F”标志位才能 binfmt_misc 在容器中正常工作,因此内核版本需要在4.8以上才可以。

CentOS 7目前使用的内核是 3.10,如果想要让 CentOS 7 构建多架构容器,那么只能够采用升级内核的方法解决,可安装 elrepo 中的 kernel-ml 内核软件包,也可自己编译内核并替换。

这里解释了我之前在 Centos7 系统下执行命令 tonistiigi/binfmt 报错的问题,因为 Centos 7 的内核太老了!

[opc@centos-7 ~]$ sudo docker run --privileged --rm tonistiigi/binfmt --install all
installing: ppc64le cannot register "/usr/bin/qemu-ppc64le" to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument
installing: arm64 cannot register "/usr/bin/qemu-aarch64" to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument
installing: arm cannot register "/usr/bin/qemu-arm" to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument
installing: s390x cannot register "/usr/bin/qemu-s390x" to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument
installing: riscv64 cannot register "/usr/bin/qemu-riscv64" to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument
installing: mips64le cannot register "/usr/bin/qemu-mips64el" to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument
installing: mips64 cannot register "/usr/bin/qemu-mips64" to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument

由于人工注册解释器的方式过于繁琐,社区的开发者们提供了专门的程序用来注册各类架构的解释器,并封装成容器镜像,镜像中就包含了注册程序以及各类 qemu 模拟程序。

此处所说的就是 tonistiigi/binfmt 镜像做的事情,接下来使用它来安装注册多架构解释器。

安装二进制文件格式解释器

先使用 docker buildx ls 查看当前系统中 Docker Buildx 支持使用的架构

$ docker buildx ls 
NAME/NODE DRIVER/ENDPOINT STATUS  BUILDKIT             PLATFORMS
default * docker
  default default         running v0.11.7+d3e6c1360f6e linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/amd64/v4, linux/386

安装二进制文件格式解释器:

$ docker run --privileged --rm tonistiigi/binfmt --install all
installing: arm64 OK
installing: arm OK
installing: s390x OK
installing: ppc64le OK
installing: riscv64 OK
installing: mips64le OK
installing: mips64 OK
...

安装后再次查看架构支持

$ docker buildx ls 
NAME/NODE DRIVER/ENDPOINT STATUS  BUILDKIT             PLATFORMS
default * docker
  default default         running v0.11.7+d3e6c1360f6e linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/amd64/v4, linux/386, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

在列表中已经可以看到 linux/arm64 等不同平台架构。

稍做补充:

buildx 插件本质上调用了 buildkit 的 api,构建是在 buildkit 的环境中进行的。是否支持多架构,取决于 buildkit 的环境,如果需要 buildkit 支持多架构,需要在宿主机执行。

通过运行 tonistiigi/binfmt 镜像并执行 --install all 命令,会注册并安装适用于不同架构的二进制文件格式解释器。这样,BuildKit 可以在构建过程中根据需要适配不同的架构,实现跨平台的构建。

创建并使用新的构建实例

使用 docker-container 驱动程序创建一个新的构建器,它可以让您访问更复杂的功能,比如多平台构建和更高级的缓存导出功能。这些功能在默认的 docker 驱动程序中目前不受支持。

$ docker buildx create --name mybuilder --bootstrap --use

在 docker buildx create 命令中,--bootstrap 是可选标志,作用是在创建完构建实例后,立即初始化这个实例,包括下载所需的镜像和运行环境,未指定时,这个实例会在后续的第一次构建任务中自动完成初始化。

交叉编译

以下步骤在 amd64 系统下执行,构建出 arm64 架构的镜像

创建示例程序

$ go mod init hello-world

main.go

package main

import (
   "fmt"
   "runtime"
)

func main() {
   fmt.Printf("the current platform architecture is %s.\n", runtime.GOARCH)
}

编译程序

$ GOARCH=arm64 GOOS=linux go build main.go

# 正常在 amd64 平台下编译出 arm64 架构的 go 可执行程序,无法正常运行。
# 但是因为前面安装 tonistiigi/binfmt,所以此处能执行,把它复制到其它 amd64 平台就无法执行了
# 虽然此处能运行,但不要依赖这个特性用于生产环境的二进制执行,性能不行
$ ./main
the current platform architecture is arm64.

$ uname -m
x86_64

构建镜像

Dockerfile

# 使用 alpine 镜像作为基础
FROM alpine:latest

# 将主程序复制到容器中
COPY main /app/main

# 设置工作目录
WORKDIR /app

# 运行主程序
CMD ["./main"]

使用 Buildx 插件构建镜像

$ docker buildx build -t hello-world --platform=linux/arm64 --load .

不指定 --load 参数,构建的镜像不会保存到本地镜像仓库,也可以指定 --push 推送到 Docker Hub 镜像仓库。

构建后使用 docker inspect hello-world:latest 查看镜像详情,可以从输出看到架构信息:

"Architecture": "arm64",
"Os": "linux",

制作多平台镜像

体验了交叉编译,接下来制作多平台的镜像

# 第一阶段:构建
FROM golang:1.21 as builder

WORKDIR /src

# 复制 Go 相关文件
COPY go.mod .
COPY main.go .

# 接受 TARGETPLATFORM 作为参数
ARG TARGETPLATFORM

# 设置环境变量来选择正确的 CROSS-COMPILE 配置
RUN export GOOS=$(echo ${TARGETPLATFORM} | cut -f1 -d /) \
 && export GOARCH=$(echo ${TARGETPLATFORM} | cut -f2 -d /) \
 && go build -o /app/main .

# 第二阶段:构建最终镜像
FROM alpine
WORKDIR /app

# 从第一阶段复制可执行文件
COPY --from=builder /app/main /app/main

ENTRYPOINT ["./main"]

TARGETPLATFORM 变量是 buildx 支持的变量,非原生 docker build 变量。

构建并推送

$ docker buildx build -t kissbug8720/hello-world:latest --platform=linux/arm64,linux/amd64 --push .

注意此处的仓库地址 "kissbug8720/hello-world" 需要替换为你自己的地址

此处需要指定 --push 参数,因为 --load 参数下不支持生成镜像清单(manifest list),会构建失败。

另外构建前先执行 docker login 登录

运行容器查看结果

访问:https://hub.docker.com/r/kissbug8720/hello-world

在页面上已经可以看到 linux/amd64 和 linux/arm64 两个架构的镜像,在不同的系统下启动容器,docker 会根据 Manifest 信息拉取当前架构的镜像运行

# 在 x86_64 架构系统运行
$ docker run --rm kissbug8720/hello-world:latest
the current platform architecture is amd64.

# 在 aarch64 架构系统运行
$ docker run --rm kissbug8720/hello-world:latest
the current platform architecture is arm64.

至此,构建多平台的镜像完成。

查看 Manifest 信息

可以使用以下命令查看镜像的 Manifest 信息。

$ docker buildx imagetools inspect kissbug8720/hello-world:latest
Name:      docker.io/kissbug8720/hello-world:latest
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:eeea71ba456ed4ad82d58bee83eb1cff81cdb499fb3dfe1529e0464d9758290b

Manifests:
  Name:        docker.io/kissbug8720/hello-world:latest@sha256:79c9b2ee932a75c6064595dc1827270bbbba6638f9de244d944574ce1c706de7
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm64

  Name:        docker.io/kissbug8720/hello-world:latest@sha256:ddd2394b302e380772bf7633894cfdceef466d55e2fcd54a4653cae598eb28e3
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/amd64

多架构镜像制作先学习到这里

参考