マルチステージビルド は,Dockerイメージのビルドを複数のステージに分割する機能です.Goのソースコードをビルドするステージと実行時のステージを分離することで,実行時のイメージサイズを軽減できます
実行時のイメージには Alpine のような軽量Linuxが使われることが多いですが,distrolessイメージも良く知られています.distrolessイメージは,Debianイメージに含まれるファイルを極限まで削減したもので,シェルすら含まれていません.
Distroless images are very small. The smallest distroless image, gcr.io/distroless/static-debian11, is around 2 MiB. That's about 50% of the size of alpine (~5 MiB), and less than 2% of the size of debian (124 MiB).
https://github.com/GoogleContainerTools/distroless
今回は実行時のイメージサイズを alpine / distrolessとした場合で比較してみます
ソースコード (Dockerfile)
スターを頂けると励みになります
builderイメージ
Goのソースコードをビルドするステージです.次の3つのステージで構成しています
- baseステージ: 各ステージ間で共通する処理を実行
- depsステージ: パッケージの依存関係を解決
- builderステージ: Goのソースコードをビルド
Goのビルド成果物 (実行ファイル) はserver
という名前にしています
FROM golang:1.19-alpine as base
# ワークディレクトリの指定
WORKDIR /app
# ----------------------------------------------------------------
# 依存関係の解決
# ----------------------------------------------------------------
FROM base as deps
# モジュールのダウンロード
COPY go.mod go.sum ./
RUN go mod download
# ----------------------------------------------------------------
# ビルド
# ----------------------------------------------------------------
FROM base as builder
COPY --from=deps /go/pkg /go/pkg
COPY . .
# 外部依存の無い実行ファイルを作る
ARG CGO_ENABLED=0
# 64bit linux用にビルドする
ARG GOOS=linux
ARG GOARCH=amd64
# ビルド成果物にデバック情報が含まれないようにする
RUN go build -ldflags '-s -w' -o ./server
実行時のイメージ (runner)
alpineイメージを利用する場合
builderステージから実行ファイルserver
をコピーし,ENTRYPOINTで実行します
# イメージのタグを指定した方が良いが,面倒なので今回は省略
FROM alpine as runner
WORKDIR /app
RUN addgroup --system --gid 10001 nonroot
RUN adduser --system --uid 10001 nonroot
COPY --from=builder --chown=nonroot:nonroot /app/server .
ENTRYPOINT ["./server"]
USER nonroot
EXPOSE 8080
distrolessイメージを利用する場合
こちらのリポジトリから利用可能なイメージを選びます.今回は最も軽量なgcr.io/distroless/static-debian12
を選びました.python, java, nodejsのランタイムが含まれるイメージも利用できます
alpineの場合と同様にserver
をコピーし,ENTRYPOINTで実行します
FROM gcr.io/distroless/static-debian12:nonroot as runner-distroless
WORKDIR /app
USER nonroot
COPY --from=builder /app/server .
ENTRYPOINT ["./server"]
EXPOSE 8080
ENTRYPOINT ./server
としてはいけません.この書き方ではシェルを介して./server
を実行しますが,distrolessイメージにはシェルが含まれないので実行できません
https://kinsta.com/jp/blog/dockerfile-entrypoint/
イメージサイズの比較
それぞれのイメージを適当な名前でビルドします
docker build . --target runner-alpine -t go-alpine
docker build . --target runner-distroless -t go-distroless
イメージサイズを確認します
docker images | grep go-
go-alpine latest 7034c38b2fcf 48 seconds ago 13.3MB
go-distroless latest e9c3cc892aff About an hour ago 6.46MB
alpineイメージが 13.3MB でdistrolessイメージが 6.46MB となりました.およそ半分にまでイメージサイズを削減できます
開発環境 (おまけ)
おまけで開発環境も載せておきます.こちらの記事を参考にしました
Goのホットリロードツールであるairを利用します
FROM golang:1.19-alpine as dev
WORKDIR /app
RUN go install github.com/cosmtrek/air@v1.40.0
ENTRYPOINT ["air"]
airの最新バージョンはgithub.com/air-verse/air
にリポジトリが変更されているので注意です
開発環境ではdocker-composeでワークスペースをvolumeマウントしています.docker-composeではビルドステージを指定するためにbuild > args
へ- target=dev
を指定します
# docker-composeの例
version: "3.3"
services:
go:
container_name: go
build:
context: .
dockerfile: Dockerfile
args:
- target=dev
volumes:
- .:/app
ports:
- 8080:8080
以上です