WHY: なぜ中間イメージをタグ付けするのか
今では Docker イメージの軽量化のためにマルチステージビルドは欠かせません。 ローカルでの実行では問題ありませんが、CI 環境などの dockerd が毎度変わる Docker In Docker(dind)では問題が起こります。
最終イメージの中にビルドプロセスの全てが含まれていれば 1 Dockerfile 1 Cache で単純な--cache-from を指定するだけで良いです。
マルチステージビルドは最終イメージの軽量化が目的であるため、最終イメージにキャッシュすべき情報はほとんど含まれておらずキャッシュしても無意味です。
キャッシュしたいステージを明示的に--target
で指定して、ビルドされたイメージを外部に保存する必要があります。
例えば、以下の Dockerfile を考えます。
stage1
とstage2
は、キャッシュしたいです。
FROM alpine:latest as stage1 LABEL stage=1 WORKDIR /workdir RUN sleep 3 && touch stage1.txt FROM alpine:latest as stage2 LABEL stage=2 WORKDIR /workdir RUN sleep 6 && touch stage2.txt COPY --from=stage1 /workdir/stage1.txt /workdir/stage1.txt FROM alpine:latest as stage3 LABEL stage=3 WORKDIR /workdir COPY --from=stage1 /workdir/stage1.txt /workdir/stage1.txt COPY --from=stage2 /workdir/stage2.txt /workdir/stage2.txt
HOW: どう中間イメージをタグ付けするのか
BuildKit を使ってビルドする場合とそうでない場合でやり方が異なります。
また、最終ステージにはCOPY
のみのため、キャッシュを利用しません。
BuildKit なし
Dockerfile
でLABEL
を使いラベル付けし、docker images
で--filter
オプションを使うことタグ付したいイメージを取得できます。
LABEL stage=1
のステージを取得するには、$(docker images --filter 'label=stage=1' -q | head -n 1)
とすれば良いです。
export DOCKER_BUILDKIT=0 # キャッシュのためにPull # &とwaitで並列実行 docker pull test:stage1 || true & docker pull test:stage2 || true & wait # ビルドする docker build --cache-from= . # 中間イメージにタグ付けする docker tag $(docker images --filter 'label=stage=1' -q | head -n 1) test:stage1 docker tag $(docker images --filter 'label=stage=2' -q | head -n 1) test:stage2 # キャッシュのためにPush # &とwaitで並列実行 docker push test:latest & docker push test:stage1 & docker push test:stage2 & wait
他の選択肢
このようにしてもできます。 キャッシュが効くのでタグ付けのためのビルドはすぐ終わるとはいえ、上のやり方の方が早いので良いです。
# ビルドする docker build -t test:latest . # 中間イメージにタグ付けする docker build -t test:stage1 --target stage1 --cache-from test:stage1 . docker build -t test:stage2 --target stage2 --cache-from test:stage2 .
BuildKit あり
最近では、BuildKit を使ってビルドするのが当たり前になっています。
BuildKit ではDockerfile
をパースしステージ間の依存関係を解釈するためstage1
とstage2
を並列にビルドでき、ビルド時間の短縮につながります。
しかし、BuildKit を使ったビルドでは中間イメージが生成されません。
docker images
しても、最終イメージしか表示されません。
理由は分かりませんが、容量を圧迫しないようにあえてそうしているのでしょうか。
そのため、BuildKit なしの場合のように、docker tag
によるタグ付けができません。
したがって、個別にビルドすることによってタグ付けします。
export DOCKER_BUILDKIT=1 # ビルドする docker build -t test:latest --cache-from=test:stage1,test:stage2 --build-arg BUILDKIT_INLINE_CACHE=1 . # 中間イメージにタグ付けする # &とwaitで並列実行 docker build -t test:stage1 --target stage1 --cache-from test:stage1 --build-arg BUILDKIT_INLINE_CACHE=1 . & docker build -t test:stage2 --target stage2 --cache-from test:stage2 --build-arg BUILDKIT_INLINE_CACHE=1 . & wait # キャッシュのためにPush # &とwaitで並列実行 docker push test:latest & docker push test:stage1 & docker push test:stage2 & wait
--cache-from
はまずレジストリからメタデータのみを取得し、キャッシュヒットの可能性があるレイヤーのみを Pull します。
BuildKit で最終ステージまでビルドしてから個別にステージをビルドすると一番無駄が少ないです。
ツールを使うことで簡単にビルドスクリプトを生成できます。 最終ステージをキャッシュするかどうかなど細かいチューニングは人の手で行ってください。
$ go get -u github.com/kotaroooo0/melancholy $ melancholy -i image_name # ----- Build image ----- docker build -t image_name:latest --cache-from=image_name:stage1,image_name:stage2,image_name:latest --build-arg BUILDKIT_INLINE_CACHE=1 . # ----- Attach tags ----- docker build -t image_name:stage1 --target=stage1 --build-arg BUILDKIT_INLINE_CACHE=1 . & docker build -t image_name:stage2 --target=stage2 --build-arg BUILDKIT_INLINE_CACHE=1 . & wait # ----- Push images ----- docker push image_name:stage1 & docker push image_name:stage2 & docker push image_name:latest & wait