【Docker】マルチステージビルドにおけるFROM命令の挙動

はじめに

Dockerイメージをビルドする際、マルチステージビルドを用いることでイメージのサイズを削減することができます。マルチステージビルドのその他の用途として、ベースステージをもとに環境に応じた複数のイメージをビルドする用途にも有効です。
今回はFROM命令で別ステージを参照した際の挙動について検証してみました。

前提条件

以下環境で検証しています。

  • macOS Monterey(12.5.1)
  • Docker Desktop 4.0.1

Dockerfileの構成

検証に使用するDockerfileは以下の通りです。

###################
# ベースステージ
###################
FROM alpine:latest AS base
ENV MSG="hello world"
RUN echo "### base ###"

###################
# 開発環境
###################
FROM base AS dev
RUN echo "### dev ###"
CMD ["sh", "-c", "echo dev && echo ${MSG}"]

###################
# 本番環境ビルダー
###################
FROM base AS prod-builder
RUN echo "### prod-builder ###"
RUN echo "${MSG} from prod" > /sample.txt

###################
# 本番環境
###################
FROM base AS prod
COPY --from=prod-builder /sample.txt sample.txt
RUN echo "### prod ###"
CMD ["sh", "-c", "echo prod && cat sample.txt"]

baseは全環境共通のベースステージです。後続のステージで使用する環境変数や実行環境などを定義します。
devは開発環境用のイメージです。FROM命令でbaseを参照して開発に必要なライブラリやツールをインストールします。CMD命令で開発用のサーバを起動します。
prod-builderは、本番環境のコードを生成するビルダーです。FROM命令でbaseを参照した後、ビルドに必要なライブラリやツールをインストールします。ソースコードをローカルからコピーしてビルド処理を実行します。
prodは本番環境用のイメージです。FROM命令でbaseを参照します。COPY命令で、prod-builderでビルドした成果物を自分のイメージにコピーし、CMD命令で本番用のサーバを起動します。

開発環境のビルドと実行

--targetオプションにdevステージを指定して開発環境用のDockerイメージをビルドします。prod-builderprodの処理は実行されず、basedevの処理内容でDockerイメージがビルドされました。

$ docker image build \
  --target dev \
  --no-cache \
  -t sample-dev:latest .
[+] Building 8.5s (7/7) FINISHED                                             
 => [internal] load build definition from Dockerfile                    0.4s
 => => transferring dockerfile: 684B                                    0.0s
 => [internal] load .dockerignore                                       0.5s
 => => transferring context: 2B                                         0.0s
 => [internal] load metadata for docker.io/library/alpine:latest        3.7s
 => CACHED [base 1/2] FROM docker.io/library/alpine:latest@sha256:bc41  0.0s
 => [base 2/2] RUN echo "### base ###"                                  1.3s
 => [dev 1/1] RUN echo "### dev ###"                                    1.5s
 => exporting to image                                                  0.9s
 => => exporting layers                                                 0.8s
 => => writing image sha256:aa37fe84da100e4842e4addde3221b1389fe8822e1  0.0s
 => => naming to docker.io/library/sample-dev:latest                    0.0s

ビルドしたDockerイメージを使用してコンテナを実行します。開発環境用のメッセージとbaseから引き継いだ環境変数が表示されました。

$ docker container run --rm sample-dev:latest
dev
hello world

本番環境のビルドと実行

--targetオプションにprodステージを指定して開発環境用のDockerイメージをビルドします。今度はdevの処理がスキップされ、baseprod-builderprodの処理内容でDockerイメージがビルドされました。

$ docker image build \
  --target prod \
  --no-cache \
  -t sample-prod:latest .
[+] Building 9.8s (10/10) FINISHED                                           
 => [internal] load build definition from Dockerfile                    0.4s
 => => transferring dockerfile: 683B                                    0.0s
 => [internal] load .dockerignore                                       0.5s
 => => transferring context: 2B                                         0.0s
 => [internal] load metadata for docker.io/library/alpine:latest        1.0s
 => CACHED [base 1/2] FROM docker.io/library/alpine:latest@sha256:bc41  0.0s
 => [base 2/2] RUN echo "### base ###"                                  1.3s
 => [prod-builder 1/2] RUN echo "### prod-builder ###"                  1.5s 
 => [prod-builder 2/2] RUN echo "hello world from prod" > /sample.txt   1.4s
 => [prod 1/2] COPY --from=prod-builder /sample.txt sample.txt          0.6s
 => [prod 2/2] RUN echo "### prod ###"                                  1.4s
 => exporting to image                                                  1.2s
 => => exporting layers                                                 1.1s
 => => writing image sha256:f8b2e2895b844537a3342173da1b8db3c8ef452cb3  0.0s
 => => naming to docker.io/library/sample-prod:latest                   0.0s

--targetを指定しないでビルドした場合も前述と同様の挙動になります。最後に書かれたprodのビルドに必要な処理のみ実行されます。

$ docker image build \                        
  --no-cache \ 
  -t sample-prod:latest .
[+] Building 17.8s (10/10) FINISHED                                          
 => [internal] load build definition from Dockerfile                    0.7s
 => => transferring dockerfile: 683B                                    0.0s
 => [internal] load .dockerignore                                       0.4s
 => => transferring context: 2B                                         0.1s
 => [internal] load metadata for docker.io/library/alpine:latest        8.7s
 => CACHED [base 1/2] FROM docker.io/library/alpine:latest@sha256:bc41  0.0s
 => [base 2/2] RUN echo "### base ###"                                  1.3s
 => [prod-builder 1/2] RUN echo "### prod-builder ###"                  1.5s
 => [prod-builder 2/2] RUN echo "hello world from prod" > /sample.txt   1.5s
 => [prod 1/2] COPY --from=prod-builder /sample.txt sample.txt          0.5s
 => [prod 2/2] RUN echo "### prod ###"                                  1.6s
 => exporting to image                                                  1.2s
 => => exporting layers                                                 1.1s
 => => writing image sha256:fcd7054aed2fc87f7399e70e1b4b9243a7d5ad7f91  0.0s
 => => naming to docker.io/library/sample-prod:latest                   0.0s

ビルドしたDockerイメージを使用してコンテナを実行します。本番環境用のメッセージと、prod-builderからコピーした成果物ファイルの内容が表示されました。

docker container run --rm sample-prod:latest  
prod
hello world from prod

まとめ

環境ごとにステージを用意して、それぞれFROM命令でベースイメージを参照することで、一つのDockerfileで複数環境のDockerイメージのビルドが実現できました。ベースステージからの参照ルートを環境ごとに分離することで不要なステージの処理をスキップでき、ビルド時間の短縮にも有効です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です