Go言語で作ったアプリケーションをコンテナイメージにするとき、以下のようにマルチステージビルドを利用したDockerfileを書くことが多いでしょう。
FROM golang:1.21-bookworm as builder WORKDIR /opt/app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -ldflags="-s -w" -o server ./cmd/server FROM gcr.io/distroless/base-debian12:nonroot COPY --from=builder --chown=nonroot:nonroot /opt/app/server /server ENV PORT 8000 EXPOSE $PORT ENTRYPOINT ["/server"]
ここで、ベースイメージである golang:1.21-bookworm
や gcr.io/distroless/base-debian12:nonroot
はマルチアーキテクチャイメージなので、--platform
オプションを利用することでこのDockerfileのままホストのアーキテクチャと異なるイメージをビルドすることができます。
例えばGitHub Actions上でbuildすると基本的にはamd64のランナーを使うことになりますが、docker/setup-qemu-action・docker/setup-buildx-action・docker/build-push-actionといったactionを利用することで、arm64向けのイメージをビルドすることができます。もちろんマルチアーキテクチャイメージを作ることも可能です。
ここで、amd64のホスト上で先ほどのDockerfileからarm64向けのイメージを作ろうとすると、最初のbuilderステージからQEMUを利用してarm64をエミュレートした環境でgo buildが行われます。エミュレーションなのでとても遅いです。自分の個人開発プロジェクトで作ったちょっとしたLambdaのイメージビルドに7分30秒もかかっていました。
今回は、これを1分30秒まで短縮する方法をご紹介します。
Go言語はクロスコンパイルが得意な言語なのに、先ほどのdocker buildの方法ではそれが活かせていないと言えます。しかし、かといってdockerの外でbuildしたバイナリをCOPYするような仕組みにするのも美しくありません。
Docker wayから外れずにGo言語の良さを活かすためのBuildxの機能がBUILDPLATFORM, TARGETARCH引数です。これらの引数はBuildKitを使う際に自動で設定されます。BUILDPLATFORMはbuild環境のplatform(例: linux/amd64
)、TARGETARCHはビルドしたい対象のアーキテクチャ(例: arm64
)です。
以下のようにDockerfileを書き換えることで、builderステージは必ずビルド環境と同じプラットフォームで実行されます。つまりこのステージでのエミュレーションが不要になり、ビルドが高速になります。一方、go build時にアーキテクチャを明示する必要が出てきたため、TARGETARCHをGOARCHとして渡してあげています。
-FROM golang:1.21-bookworm as builder +FROM --platform=$BUILDPLATFORM golang:1.21-bookworm as builder +ARG TARGETARCH WORKDIR /opt/app COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN go build -ldflags="-s -w" -o server ./cmd/server +RUN GOARCH=${TARGETARCH} go build -ldflags="-s -w" -o server ./cmd/server FROM gcr.io/distroless/base-debian12:nonroot COPY --from=builder --chown=nonroot:nonroot /opt/app/server /server ENV PORT 8000 EXPOSE $PORT ENTRYPOINT ["/server"]