使用 buildx 構建跨平臺鏡像
構建跨平臺鏡像是 Docker 生態系統中的一個重要話題,因爲跨平臺鏡像可以在多種平臺上運行,極具靈活性。爲了實現這個目標,Docker 社區提供了多種方式來構建跨平臺鏡像,其中之一是使用 docker manifest
,我在《使用 docker manifest 構建跨平臺鏡像》一文中詳細介紹了這種方法。然而,目前最流行的方式是使用 Docker 的 buildx
工具,這種方式不僅可以輕鬆構建跨平臺鏡像,還可以自動化整個構建過程,大大提高了效率。在本文中,我們將重點介紹使用 buildx
構建跨平臺鏡像的方法和技巧。
簡介
buildx
是 Docker 官方提供的一個構建工具,它可以幫助用戶快速、高效地構建 Docker 鏡像,並支持多種平臺的構建。使用 buildx
,用戶可以在單個命令中構建多種架構的鏡像,例如 x86 和 ARM 架構,而無需手動操作多個構建命令。此外,buildx
還支持 Dockerfile 的多階段構建和緩存,這可以大大提高鏡像構建的效率和速度。
安裝
buildx
是一個管理 Docker 構建的 CLI 插件,底層使用 BuildKit 擴展了 Docker 構建功能。
筆記:
BuildKit
是 Docker 官方提供的一個高性能構建引擎,可以用來替代 Docker 原有的構建引擎。相比於原有引擎,BuildKit
具有更快的構建速度、更高的並行性、更少的資源佔用和更好的安全性。
要安裝並使用 buildx
,需要 Docker Engine 版本號大於等於 19.03。
如果你使用的是 Docker Desktop,則默認安裝了 buildx
。可以使用 docker buildx version
命令查看安裝版本,得到以下類似輸出,證明已經安裝過了。
$ docker buildx version
github.com/docker/buildx v0.9.1 ed00243a0ce2a0aee75311b06e32d33b44729689
如果需要手動安裝,可以從 GitHub 發佈頁面下載對應平臺的最新二進制文件,重命名爲 docker-buildx
,然後將其放到 Docker 插件目錄下(Linux/Mac 系統爲 $HOME/.docker/cli-plugins
,Windows 系統爲 %USERPROFILE%\.docker\cli-plugins
)。
Linux/Mac 系統還需要給插件增加可執行權限 chmod +x ~/.docker/cli-plugins/docker-buildx
,之後就可以使用 buildx
了。
更詳細的安裝過程可以參考官方文檔。
構建跨平臺鏡像
首先,需要澄清的是,本文中所提到的「跨平臺鏡像」這一說法並不十分準確。實際上,Docker 官方術語叫 Multi-platform images
即「多平臺鏡像」,意思是支持多種不同 CPU 架構的鏡像。之所以使用「跨平臺鏡像」這一術語,是因爲從使用者的角度來看,在使用如 docker pull
、docker run
等命令來拉取和啓動容器時,並不會感知到這個鏡像是一個虛擬的 manifest list
鏡像還是針對當前平臺的鏡像。
筆記:
manifest list
是通過指定多個鏡像名稱創建的鏡像列表,是一個虛擬鏡像,它包含了多個不同平臺的鏡像信息。可以像普通鏡像一樣使用docker pull
和docker run
等命令來操作它。如果你想了解關於manifest list
的更多信息,可參考《使用 docker manifest 構建跨平臺鏡像》一文。
跨平臺鏡像構建策略
builder
支持三種不同策略構建跨平臺鏡像:
- 在內核中使用 QEMU 仿真支持。
如果你正在使用 Docker Desktop,則已經支持了 QEMU,QEMU 是最簡單的構建跨平臺鏡像策略。它不需要對原有的 Dockerfile
進行任何更改,BuildKit
會通過 binfmt_misc 這一 Linux 內核功能實現跨平臺程序的執行。
工作原理:
QEMU 是一個處理器模擬器,可以模擬不同的 CPU 架構,我們可以把它理解爲是另一種形式的虛擬機。在 buildx
中,QEMU 用於在構建過程中執行非本地架構的二進制文件。例如,在 x86 主機上構建一個 ARM 鏡像時,QEMU 可以模擬 ARM 環境並運行 ARM 二進制文件。
binfmt_misc 是 Linux 內核的一個模塊,它允許用戶註冊可執行文件格式和相應的解釋器。當內核遇到未知格式的可執行文件時,會使用 binfmt_misc 查找與該文件格式關聯的解釋器(在這種情況下是 QEMU)並運行文件。
QEMU 和 binfmt_misc 的結合使得通過 buildx
跨平臺構建成爲可能。這樣我們就可以在一個架構的主機上構建針對其他架構的 Docker 鏡像,而無需擁有實際的目標硬件。
雖然 Docker Desktop 預配置了 binfmt_misc 對其他平臺的支持,但對於其他版本 Docker,你可能需要使用 tonistiigi/binfmt
鏡像啓動一個特權容器來進行支持:
$ docker run --privileged --rm tonistiigi/binfmt --install all
- 使用相同的構建器實例在多個本機節點上構建。
此方法直接在對應平臺的硬件上構建鏡像,所以需要準備各個平臺的主機。因爲此方法門檻比較高,所以並不常使用。
- 使用 Dockerfile 中的多階段構建,交叉編譯到不同的平臺架構中。
交叉編譯的複雜度不在於 Docker,而是取決於程序本身。比如 Go 程序就很容易實現交叉編譯,只需要在使用 go build
構建程序時指定 GOOS
、GOARCH
兩個環境變量即可實現。
創建 builder
要使用 buildx
構建跨平臺鏡像,我們需要先創建一個 builder
,可以翻譯爲「構建器」。
使用 docker buildx ls
命令可以查看 builder
列表:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default * docker
default default running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux docker
desktop-linux desktop-linux running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
這兩個是默認 builder
,default *
中的 *
表示當前正在使用的 builder
,當我們運行 docker build
命令時就是在使用此 builder
構建鏡像。
可以發現,這兩個默認的 builder
第二列 DRIVER/ENDPOINT
項的值都是 docker
,表示它們都使用 docker
驅動程序。
buildx
支持以下幾種驅動程序:
默認的 docker
驅動程序優先考慮簡單性和易用性,所以它對緩存和輸出格式等高級功能的支持有限,並且不可配置。其他驅動程序則提供了更大的靈活性,並且更擅長處理高級場景。
具體差異你可以到官方文檔中查看。
因爲使用 docker
驅動程序的默認 builder
不支持使用單條命令(默認 builder
的 --platform
參數只接受單個值)構建跨平臺鏡像,所以我們需要使用 docker-container
驅動創建一個新的 builder
。
命令語法如下:
$ docker buildx create --name=<builder-name> --driver=<driver> --driver-opt=<driver-options>
參數含義如下:
-
--name
:構建器名稱,必填。 -
--driver
:構建器驅動程序,默認爲docker-container
。 -
--driver-opt
:驅動程序選項,如選項--driver-opt=image=moby/buildkit:v0.11.3
可以安裝指定版本的BuildKit
,默認值是 moby/buildkit。
更多可選參數可以參考官方文檔。
我們可以使用如下命令創建一個新的 builder
:
$ docker buildx create --name mybuilder
mybuilder
再次查看 builder
列表:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder * docker-container
mybuilder0 unix:///var/run/docker.sock inactive
default docker
default default running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux docker
desktop-linux desktop-linux running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
可以發現選中的構建器已經切換到了 mybuilder
,如果沒有選中,你需要手動使用 docker buildx use mybuilder
命令切換構建器。
啓動 builder
我們新創建的 mybuilder
當前狀態爲 inactive
,需要啓動才能使用。
$ docker buildx inspect --bootstrap mybuilder
[+] Building 16.8s (1/1) FINISHED
=> [internal] booting buildkit 16.8s
=> => pulling image moby/buildkit:buildx-stable-1 16.1s
=> => creating container buildx_buildkit_mybuilder0 0.7s
Name: mybuilder
Driver: docker-container
Nodes:
Name: mybuilder0
Endpoint: unix:///var/run/docker.sock
Status: running
Buildkit: v0.9.3
Platforms: linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
inspect
子命令用來檢查構建器狀態,使用 --bootstrap
參數則可以啓動 mybuilder
構建器。
再次查看 builder
列表,mybuilder
狀態已經變成了 running
。
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder * docker-container
mybuilder0 unix:///var/run/docker.sock running v0.9.3 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
default docker
default default running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux docker
desktop-linux desktop-linux running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
其中 PLATFORMS
一列所展示的值 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
就是當前構建器所支持的所有平臺了。
現在使用 docker ps
命令可以看到 mybuilder
構建器所對應的 BuildKit
容器已經啓動。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b8887f253d41 moby/buildkit:buildx-stable-1 "buildkitd" 4 minutes ago Up 4 minutes buildx_buildkit_mybuilder0
這個容器就是輔助我們構建跨平臺鏡像用的,不要手動刪除它。
使用 builder
構建跨平臺鏡像
現在一些準備工作已經就緒,我們終於可以使用 builder
構建跨平臺鏡像了。
這裏以一個 Go 程序爲例,來演示如何構建跨平臺鏡像。
hello.go
程序如下:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("Hello, %s/%s!\n", runtime.GOOS, runtime.GOARCH)
}
這個程序非常簡單,執行後打印 Hello, 操作系統/CPU 架構
。
Go 程序還需要一個 go.mod
文件:
module hello
go 1.20
編寫 Dockerfile
內容如下:
FROM golang:1.20-alpine AS builder
WORKDIR /app
ADD . .
RUN go build -o hello .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]
這是一個普通的 Dockerfile
文件,爲了減小鏡像大小,使用了多階段構建。它跟構建僅支持當前平臺的鏡像所使用的 Dockerfile
沒什麼兩樣。
$ ls
Dockerfile go.mod hello.go
以上三個文件需要放在同一個目錄下,然後就可以在這個目錄下使用 docker buildx
來構建跨平臺鏡像了。
$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go .
docker buildx build
語法跟 docker build
一樣,--platform
參數表示構建鏡像的目標平臺,-t
表示鏡像的 Tag,.
表示上下文爲當前目錄。
唯一不同的是對 --platform
參數的支持,docker build
的 --platform
參數只支持傳遞一個平臺信息,如 --platform linux/arm64
,也就是一次只能構建單個平臺的鏡像。
而使用 docker buildx build
構建鏡像則支持同時傳遞多個平臺信息,中間使用英文逗號分隔,這樣就實現了只用一條命令便可以構建跨平臺鏡像的功能。
執行以上命令後,我們將會得到一條警告:
WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load
這條警告提示我們沒有爲 docker-container
驅動程序指定輸出,生成結果將只會保留在構建緩存中,使用 --push
可以將鏡像推送到 Docker Hub 遠程倉庫,使用 --load
可以將鏡像保存在本地。
這是因爲我們新創建的 mybuilder
是啓動了一個容器來運行 BuildKit
,它並不能直接將構建好的跨平臺鏡像輸出到本機或推送到遠程,必須要用戶來手動指定輸出位置。
我們可以嘗試指定 --load
將鏡像保存的本地主機。
$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go . --load
[+] Building 0.0s (0/0)
ERROR: docker exporter does not currently support exporting manifest lists
結果會得到一條錯誤日誌。看來它並不支持直接將跨平臺鏡像輸出到本機,這其實是因爲傳遞了多個 --platform
的關係,如果 --platform
只傳遞了一個平臺,則可以使用 --load
將構建好的鏡像輸出到本機。
那麼我們就只能通過 --push
參數將跨平臺鏡像推送到遠程倉庫了。不過在此之前需要確保使用 docker login
完成登錄。
$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go . --push
現在登錄 Docker Hub 就可以看見推送上來的跨平臺鏡像了。
我們也可以使用 imagetools
來檢查跨平臺鏡像的 manifest
信息。
$ docker buildx imagetools inspect jianghushinian/hello-go
Name: docker.io/jianghushinian/hello-go:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: sha256:51199dadfc55b23d6ab5cfd2d67e38edd513a707273b1b8b554985ff562104db
Manifests:
Name: docker.io/jianghushinian/hello-go:latest@sha256:8032a6f23f3bd3050852e77b6e4a4d0a705dfd710fb63bc4c3dc9d5e01c8e9a6
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm64
Name: docker.io/jianghushinian/hello-go:latest@sha256:fd46fd7e93c7deef5ad8496c2cf08c266bac42ac77f1e444e83d4f79d58441ba
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/amd64
可以看到,這個跨平臺鏡像包含了兩個目標平臺的鏡像,分別是 linux/arm64
和 linux/amd64
。
我們分別在 Apple M2 芯片平臺和 Linux x86 平臺來啓動這個 Docker 鏡像看下輸出結果。
$ docker run --rm jianghushinian/hello-go
Hello, linux/arm64!
$ docker run --rm jianghushinian/hello-go
Hello, linux/amd64!
至此,我們使用 builder
完成了跨平臺鏡像的構建。
使用交叉編譯
以上演示的構建跨平臺鏡像過程就是利用 QEMU 的能力,因爲 Go 語言的交叉編譯非常簡單,所以我們再來演示一下如何使用交叉編譯來構建跨平臺鏡像。
我們只需要對 Dockerfile
文件進行修改:
FROM --platform=$BUILDPLATFORM golang:1.20-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
ADD . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o hello .
FROM --platform=$TARGETPLATFORM alpine:latest
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]
其中 BUILDPLATFORM
、TARGETOS
、TARGETARCH
、TARGETPLATFORM
四個變量是 BuildKit
提供的全局變量,分別表示構建鏡像所在平臺、操作系統、架構、構建鏡像的目標平臺。
在構建鏡像時,BuildKit
會將當前所在平臺信息傳遞給 Dockerfile
中的 BUILDPLATFORM
參數(如 linux/arm64
)。
通過 --platform
參數傳遞的 linux/arm64,linux/amd64
鏡像目標平臺列表會依次傳遞給 TARGETPLATFORM
變量。
而 TARGETOS
、TARGETARCH
兩個變量在使用時則需要先通過 ARG
進行聲明,BuildKit
會自動爲其賦值。
在 Go 程序進行編譯時,可以通過 GOOS
環境變量指定目標操作系統,通過 GOARCH
環境變量指定目標架構。
所以這個 Dockerfile
所表示的含義是:首先拉取當前構建鏡像所在平臺的 golang
鏡像,然後使用交叉編譯構建目標平臺的 Go 程序,最後將構建好的 Go 程序複製到目標平臺的 alpine
鏡像。
最終我們會通過交叉編譯得到一個跨平臺鏡像。
筆記:通過
FROM --platform=$BUILDPLATFORM image
可以拉取指定平臺的鏡像,由此我們可以知道,其實 golang 和 alpine 鏡像都是支持跨平臺的。
構建鏡像命令不變:
$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-cross-go . --push
啓動鏡像後輸出結果不變:
$ docker run --rm jianghushinian/hello-cross-go
Hello, linux/arm64!
$ docker run --rm jianghushinian/hello-cross-go
Hello, linux/amd64!
至此,我們利用 Go 語言的交叉編譯完成了跨平臺鏡像的構建。
平臺相關的全局變量
關於上面提到的幾個全局變量,BuildKit
後端預定義了一組 ARG 全局變量(共 8 個)可供使用,其定義和說明如下:
使用示例如下:
# 這裏可以直接使用 TARGETPLATFORM 變量
FROM --platform=$TARGETPLATFORM alpine
# 稍後的 RUN 命令想要使用變量必須提前用 ARG 進行聲明
ARG TARGETPLATFORM
RUN echo "I'm building for $TARGETPLATFORM"
刪除 builder
我們已經實現了使用 builder
構建跨平臺鏡像。如果現在你想要恢復環境,刪除新建的 builder
。則可以使用 docker buildx rm mybuilder
命令來完成。
$ docker buildx rm mybuilder
mybuilder removed
跟隨 mybuilder
啓動的 buildx_buildkit_mybuilder0
容器也會隨之被刪除。
現在再使用 docker buildx ls
命令查看構建器列表,已經恢復成原來的樣子了。
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default * docker
default default running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux docker
desktop-linux desktop-linux running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
功能清單
除了前文介紹的幾個 buildx
常用命令,更多功能可以通過 --help
參數進行查看。
$ 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.
如 stop
、rm
可以管理 builder
的生命週期。每條子命令又可以使用 docker buildx COMMAND --help
方式查看使用幫助,感興趣的同學可以自行學習。
總結
本文講解了如何使用 buildx
構建跨平臺鏡像,這也是在 Docker 生態中目前最優的構建方式。
首先介紹了 buildx
是什麼,以及如何安裝。接下來就進入了構建跨平臺鏡像的講解,我們分析了三種跨平臺鏡像構建策略,並且分別對 QEMU 和 交叉編譯兩種策略進行了演示。QEMU 策略無需對 Dockerfile
做任何更改,而使用交叉編譯方式則需要根據程序的支持來編寫 Dockerfile
構建跨平臺應用。
最後我們還講解了如何管理 buildx
的生命週期,以及羅列了 buildx
的功能清單幫助你進一步深入學習。
參考
-
buildx 倉庫地址:https://github.com/docker/buildx
-
buildx 安裝文檔:https://docs.docker.com/build/install-buildx/
-
buildx 文檔:https://docs.docker.com/engine/reference/commandline/buildx/
-
buildx 驅動程序:https://docs.docker.com/build/drivers/
-
多平臺鏡像:https://docs.docker.com/build/building/multi-platform/
-
多階段構建:https://docs.docker.com/build/building/multi-stage/
-
buildkit 文檔:https://docs.docker.com/build/buildkit/
-
buildkit 支持的全局可用變量:https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/OvOIjj4ser-QWd2BsfsY-Q