Go - 跨平台构建Docker镜像
1. 概述
Docker镜像支持多平台,即单个镜像可支持不同的OS和CPU架构,例如linux/amd64
、linux/arm64
等。
在DockerHub,可以查看每个镜像支持的操作系统和处理器架构,如下图,python支持windows和linux两个操作系统,多种CPU架构。
ubuntu
在不同的操作系统和CPU架构下,通过docker pull
或docker run
,docker daemon
会帮助我们自动选择适合的镜像,非常方便。
那么,如何build不同CPU架构的镜像呢,本文重点来介绍。
2. Buildx
集成了Buildx的BuildKit,可以实现跨平台的镜像构建,只需要通过--platform
指定对应的平台即可,例如linux/amd64
、linux/arm64
、darwin/amd64
。
下面,先从一些术语来介绍其功能。
2.1. builder or builder实例
通过docker buildx ls
可以查看,也支持create
、rm
等操作
% docker buildx --help
Usage: docker buildx [OPTIONS] COMMAND
Extended build capabilities with BuildKit
Options:
--builder string Override the configured builder instance
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
du Disk usage
inspect Inspect current builder instance
ls List builder instances
prune Remove build cache
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.
2.2. builder node
一个builder可以包含多个node,作用:可提高build效率,也可支持更复杂的CPU架构下镜像构建。
实例创建之后可以添加新的节点,通过docker buildx create
命令的--append
选项可将--node <node>
节点加入到--name <builder>
选项指定的 builder 实例。如下将把一个远程节点加入 builder 实例:
$ docker buildx create --driver docker-container --platform linux/amd64 --name multi-builder
multi-builder
$ export DOCKER_HOST=tcp://10.10.150.66:2375
$ docker buildx create --name multi-builder --append --node remote-builder
刚创建的 builder 处于 inactive 状态,可以在 create 或 inspect 子命令中添加 --bootstrap 选项立即启动实例(可验证节点是否可用):
$ docker buildx inspect --bootstrap multi-builder
[+] Building 3.9s (2/2) FINISHED
=> [remote-builder internal] booting buildkit 3.9s
=> => pulling image moby/buildkit:buildx-stable-1 2.8s
=> => creating container buildx_buildkit_remote-builder 1.2s
=> [multi-builder0 internal] booting buildkit 3.7s
=> => pulling image moby/buildkit:buildx-stable-1 2.4s
=> => creating container buildx_buildkit_multi-builder0 1.3s
Name: multi-builder
Driver: docker-container
Nodes:
Name: multi-builder0
Endpoint: unix:///var/run/docker.sock
Status: running
Buildkit: v0.10.5
Platforms: linux/amd64*, linux/386
Name: remote-builder
Endpoint: tcp://10.10.88.20:2375
Status: running
Buildkit: v0.10.5
Platforms: linux/arm64
docker buildx ls
将列出所有可用的 builder 实例和实例中的节点:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
multi-builder docker-container
multi-builder0 unix:///var/run/docker.sock running v0.10.5 linux/amd64*, linux/386
remote-builder tcp://10.10.88.20:2375 running v0.10.5 linux/arm64
default * docker
default default running linux/amd64, linux/386
如上就创建了一个支持多平台架构的 builder 实例,执行docker buildx use <builder>
将切换到所指定的 builder 实例。
docker buildx inspect
、docker buildx stop
和 docker buildx rm
命令用于管理一个实例的生命周期。
参考:如何使用 docker buildx 构建跨平台 Go 镜像 | Shall We Code? (waynerv.com)
2.3. builder driver
buildx 实例通过两种方式来执行构建任务,两种执行方式被称为使用不同的驱动
:
- docker 驱动:使用 Docker 服务程序中集成的 BuildKit 库执行构建
- docker-container 驱动:启动一个包含 BuildKit 的容器并在容器中执行构建
docker驱动
无法使用一小部分 buildx 的特性,例如不能在一次运行中同时构建多个平台镜像
。此外,在镜像的默认输出格式上也有所区别:docker驱动
默认将构建结果以 Docker 镜像格式直接输出到 docker 的镜像目录(通常是 /var/lib/overlay2),之后执行 docker images 命令可以列出所输出的镜像。而,docker container
则需要通过--output
选项指定输出格式为镜像或其他格式。
为了一次性构建多个平台
的镜像,本文使用docker container
作为默认的 builder 实例驱动。
2.4. dockerfile中的环境变量
- BUILDPLATFORM,构建节点的平台信息,例如
linux/amd64
、linux/arm64
- BUILDARCH
- BUILDOS
- TARGETPLATFORM,目标平台信息
- TARGETARCH
- TARGETOS
以下面的dockerfile为例,可以看到对应的env值:
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log
FROM alpine
COPY --from=build /log /log
build命令:docker buildx build --platform linux/amd64,linux/arm64 -t test:0 -o type=oci,dest=./oci-image .
2.5. output
执行构建命令时,除了指定镜像名称,另外两个重要的选项是指定目标平台
和输出格式
。
当使用 docker-container
驱动时,--platform
可以接受用逗号分隔的多个值作为输入以同时指定多个目标平台,所有平台的构建结果将合并为一个整体的镜像列表
作为输出,因此,无法直接
输出为本地的 docker images 镜像。
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 的精简表示
常用的也就两种方式,分别是-o type=registry
和-o type=docker,dest=./linux-amd64-image
。
第一种,支持同时build多个镜像,第二种每次只能build一个特定平台的镜像。
docker buildx build --platform linux/amd64,linux/arm64 -t test:0 -o type=registry
# 上面的命令等价与下面两条命令
docker buildx build --platform linux/amd64 -t test:0 -o type=docker,dest=./linux-amd64-image .
docker buildx build --platform linux/arm64 -t test:0 -o type=docker,dest=./linux-arm64-image .
% docker buildx build --platform linux/amd64 -t test:0 -o type=docker,dest=./linux-amd64-image .
[+] Building 7.8s (11/11) FINISHED
=> [internal] load build definition from dockerfile 0.0s
=> => transferring dockerfile: 250B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 7.6s
=> [internal] load metadata for docker.io/library/golang:alpine 7.4s
=> [auth] library/golang:pull token for registry-1.docker.io 0.0s
=> [auth] library/alpine:pull token for registry-1.docker.io 0.0s
=> [build 1/2] FROM docker.io/library/golang:alpine@sha256:913de96707b0460bcfdfe422796bb6e559fc300f6c53286777805a9a3010a5ea 0.0s
=> => resolve docker.io/library/golang:alpine@sha256:913de96707b0460bcfdfe422796bb6e559fc300f6c53286777805a9a3010a5ea 0.0s
=> [stage-1 1/2] FROM docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 0.0s
=> => resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126 0.0s
=> CACHED [build 2/2] RUN echo "I am running on linux/arm64, building for linux/amd64" > /log 0.0s
=> CACHED [stage-1 2/2] COPY --from=build /log /log 0.0s
=> exporting to docker image format 0.1s
=> => exporting layers 0.0s
=> => exporting manifest sha256:5f55c6c58da4636aa38363b1aed04cc2ff1921bfb12c42aa308430a8a20c12ac 0.0s
=> => exporting config sha256:f2ce60afc09e4bb74d905f6b1a58504c140f2090a1420d275e89381a36c9947e 0.0s
=> => sending tarball 0.1s
% ll
total 4108
-rw-r--r-- 1 shuzhang staff 211 5 7 16:34 dockerfile
-rw-r--r-- 1 shuzhang staff 3384832 5 7 17:26 linux-amd64-image
% docker load < linux-amd64-image
f1417ff83b31: Loading layer [==================================================>] 3.375MB/3.375MB
c5ddf0bc21cf: Loading layer [==================================================>] 148B/148B
Loaded image: test:0
% docker images | grep test
test 0 f2ce60afc09e 50 minutes ago 7.05MB
3. Example
下面将以一个简单的 Go 项目作为示例,假设示例程序文件 main.go 内容如下:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("Hello world!")
fmt.Printf("Running in [%s] architecture.\n", runtime.GOARCH)
}
定义构建过程的 Dockerfile 如下:
FROM --platform=$BUILDPLATFORM golang:1.14 as builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY main.go /app/main.go
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -a -o output/main main.go
FROM alpine:latest
WORKDIR /root
COPY --from=builder /app/output/main .
CMD /root/main
构建过程分为两个阶段:
- 在一阶段中,我们将拉取一个和当前构建节点相同平台的 golang 镜像,并使用 Go 的交叉编译特性将其编译为目标架构的二进制文件。
- 然后拉取目标平台的 alpine 镜像,并将上一阶段的编译结果拷贝到镜像中。
4. Setup builder
Docker Desktop provides binfmt_misc
multi-architecture support, which means you can run containers for different Linux architectures such as arm
, mips
, ppc64le
, and even s390x
.
对于没有Desktop的服务器而言,最新的docker版本默认支持buildx,可能需要安装binfmt,以支持跨平台构建。
# 检查我们当前的buildx可以使用的全部的构建实例/构建节点,我们发现还是使用默认的,没有任何改变
[thinktik@thinkdev ~]$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
default * docker
default default running linux/amd64, linux/386
# 安装binfmt,它内置了QEMU能提供跨平台构建功能
[thinktik@thinkdev ~]$ docker run --privileged --rm tonistiigi/binfmt --install all
Unable to find image 'tonistiigi/binfmt:latest' locally
latest: Pulling from tonistiigi/binfmt
2b4d0e08bd75: Pull complete
c331be51c382: Pull complete
Digest: sha256:5bf63a53ad6222538112b5ced0f1afb8509132773ea6dd3991a197464962854e
Status: Downloaded newer image for tonistiigi/binfmt:latest
installing: s390x OK
installing: riscv64 OK
installing: mips64le OK
installing: mips64 OK
installing: arm64 OK
installing: arm OK
installing: ppc64le OK
{
"supported": [
"linux/amd64",
"linux/arm64",
"linux/riscv64",
"linux/ppc64le",
"linux/s390x",
"linux/386",
"linux/mips64le",
"linux/mips64",
"linux/arm/v7",
"linux/arm/v6"
],
"emulators": [
"qemu-aarch64",
"qemu-arm",
"qemu-mips64",
"qemu-mips64el",
"qemu-ppc64le",
"qemu-riscv64",
"qemu-s390x"
]
}
# 创建一个buildx构建器
[thinktik@thinkdev ~]$ docker buildx create --name crossbuilder --driver docker-container
crossbuilder
# 使用新创建的构建器
[thinktik@thinkdev ~]$ docker buildx use crossbuilder
# 再次检查buildx可以使用的构建实例/构建节点,我们看到新加了一个crossbuilder,并且显示支持了一大批CPU架构
[thinktik@thinkdev ~]$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
crossbuilder * docker-container
crossbuilder0 unix:///var/run/docker.sock inactive
default docker
default default running linux/amd64, linux/386, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/arm/v7, linux/arm/v6