TL;DR
マルチステージビルドな Dockerfile で ARG を使う場合は、スコープがステージ内に閉じていることに注意が必要です!ちゃんと各ステージごとに使う ARG を宣言しましょう!
また、複数のステージで使う ARG にデフォルト値があるのなら、グローバルな ARG を定義してそこにデフォルト値を記載しましょう!
こんな感じで作ればオッケーです!
# 各ステージで共通して利用する ARG にデフォルト値があるのなら、最初の FROM より前に定義する。
ARG YOUR_ARG="Default value"
FROM alpine:latest as first_stage
# ARG は、利用する各ステージごとに宣言する必要がある。
ARG YOUR_ARG
RUN echo "1st stage: ${YOUR_ARG}"
FROM alpine:latest as second_stage
# ARG は、利用する各ステージごとに宣言する必要がある。
ARG YOUR_ARG
RUN echo "2nd stage: ${YOUR_ARG}"
マルチステージビルドと ARG の関係性について
マルチステージビルドでは、 ARG や ENV のスコープはステージごとに制限されているらしいです。
皆さんご存知でしたか?わたくしは作った Dockerfile のステージ内で ARG の値を参照できていないことに気づいて初めて知りました。
こちらのコメントによれば、
Correct, Dockerfile instructions, including ENV vars and ARG are scoped per build-stage, and will not be preserved in the next stage; this is by design.
You can, however, set a global ARG (set before the first build-stage), and use that value in each build-stage;
(意訳)ENV や ARG を含む Dockerfile の命令はビルドステージごとのスコープになっており、次のステージに持ち越されません。これは設計上そのようになっています。しかしながら、グローバルな ARG(最初のビルドステージより前で定義する)を使えば、ビルドステージごとにその値を利用することができます。
とのことです。マジか。
公式ドキュメントにもしっかり載ってましたわ。
ARG 命令の変数スコープは、それが定義されたビルドステージが終了するときまでです。 複数のビルドステージにおいて ARG を利用する場合は、個々に ARG 命令を指定する必要があります。
最初にドキュメント読んだときは、ビルドステージの意味も分からないまま「あーそーゆーことね完全に理解した」と思ったような気が。。。
ARG の動きを見てみる
では実際にマルチステージビルドでの ARG の動きを見ていきましょう。
本稿の動作確認環境
本稿の執筆にあたっては、下記のバージョンで動作を確認しています。
# docker --version
Docker version 18.09.1, build 4c52b90
Dockerfile
今回の検証で使う Dockerfile は次の通りです。
# ARG1 は、グローバルに宣言し、グローバルなデフォルト値を設定する。
ARG ARG1="arg1 global default value"
# ARG2 は、グローバルに宣言するが、グローバルなデフォルト値は設定しない。
ARG ARG2
# ARG3 は、グローバルに宣言しない。
# ARG ARG3
FROM alpine:latest as first_stage
# first_stage では、各 ARG を宣言し、スコープ内のデフォルト値を設定する。
ARG ARG1="arg1 first stage value"
ARG ARG2="arg2 first stage value"
ARG ARG3="arg3 first stage value"
RUN echo -e "first_stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
FROM alpine:latest as second_stage
# second_stage では、各 ARG を宣言するが、スコープ内のデフォルト値は設定しない。
ARG ARG1
ARG ARG2
ARG ARG3
RUN echo -e "second_stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
FROM alpine:latest as third_stage
# third_stage では、すべての ARG を宣言しない。
# ARG ARG1
# ARG ARG2
# ARG ARG3
RUN echo -e "third_stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
各 ARG は下記のように設定しています。
ARG 名 | 設定内容 |
---|---|
ARG1 | グローバルな ARG として設定。デフォルト値も指定。 |
ARG2 | グローバルな ARG として設定。デフォルト値は指定しない。 |
ARG3 | グローバルな ARG として設定しない。 |
また、各ステージは次のように設定しています。
Stage 名 | 設定内容 |
---|---|
first_stage | 各 ARG を宣言。ステージ内のデフォルト値を設定。 |
second_stage | 各 ARG を宣言。ステージ内のデフォルト値は設定しない。 |
third_stage | ARG を宣言しない。 |
--build-arg
指定なしでビルド
Docker Image のビルド時に、 --build-arg
による値の指定を行わずにビルドしてみます。
# docker build . --no-cache
Sending build context to Docker daemon 14.85kB
...snip...
Step 7/14 : RUN echo -e "1st stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
---> Running in 2bbe78634ee8
first_stage:
ARG1=arg1 first stage value
ARG2=arg2 first stage value
ARG3=arg3 first stage value
...snip...
Step 12/14 : RUN echo -e "2nd stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
---> Running in 0c28af93ea9b
second_stage:
ARG1=arg1 global default value
ARG2=
ARG3=
...snip...
Step 14/14 : RUN echo -e "3rd stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
---> Running in cbca9ed88691
third_stage:
ARG1=
ARG2=
ARG3=
...snip...
- first_stage では、いずれもステージ内のデフォルト値となりました。 ARG1 を見ると、グローバルなデフォルト値よりもステージ内のデフォルト値が優先されていることがわかります。これは、よりスコープが狭い方の値を優先的に使う動きになっており、理にかなっている動きと言えるでしょう。
- second_stage では、 ARG1 のみグローバルなデフォルト値が表示されています。ステージ内で ARG を宣言はしたものの、何も値を指定しなければグローバルなデフォルト値を利用するということですね。 ARG2, ARG3 には何も表示されていませんが、これはその値を指定している場所がどこにもないからですね。
- third_stage ではすべての値が表示されていません。 ARG1 のグローバルなデフォルト値すら表示されていないということは、 ARG を利用するためにはそのステージごとに宣言する必要があるということでしょう。
--build-arg
指定ありでビルド
今度は --build-arg
による値の指定ありでビルドしてみます。
# docker build --build-arg ARG1="build arg1 value" --build-arg ARG2="build arg2 value" --build-arg ARG3="build arg3 value" . --no-cache
Sending build context to Docker daemon 14.85kB
...snip...
Step 7/14 : RUN echo -e "1st stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
---> Running in 10b37c5a524b
first_stage:
ARG1=build arg1 value
ARG2=build arg2 value
ARG3=build arg3 value
...snip...
Step 12/14 : RUN echo -e "2nd stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
---> Running in e70e3ff9fe9b
second_stage:
ARG1=build arg1 value
ARG2=build arg2 value
ARG3=build arg3 value
...snip...
Step 14/14 : RUN echo -e "3rd stage:\n\tARG1=${ARG1}\n\tARG2=${ARG2}\n\tARG3=${ARG3}"
---> Running in e675e8f648e8
third_stage:
ARG1=
ARG2=
ARG3=
...snip...
- first_stage, second_stage ともにすべての値が
--build-arg
で指定した値になりました。これは公式ドキュメントの ARG のデフォルト値で説明されている内容と一致しますね。--build-arg
で指定された値はグローバルなデフォルト値及びステージ内のデフォルト値のどちらよりも優先されます。 - third_stage ではまたもやすべての値が表示されていません。
--build-arg
で値が指定されていても、そのステージ内で宣言されていなければやはり利用できないようです。
まとめ
- マルチステージビルドな Dockerfile で ARG を利用する際には、すべてのステージでそれぞれ利用する ARG を宣言する必要がある。
- 複数のステージで利用している ARG のデフォルト値を定義したければ、グローバルな ARG を定義(最初の FROM よりも前に定義)してそこにデフォルト値を記載すると良い。
- ARG の値の優先度は
-
--build-arg
での指定値 [優先度高] - ステージ内のデフォルト値
- グローバルなデフォルト値 [優先度低]
-
おしまい。
宣伝
マルチステージビルドの ARG の挙動に気づくきっかけとなったプロジェクトの宣伝です。
Minecraft の Bedrock Server を楽に運用しようぜ!という趣旨のプロジェクトです。
Minecraft ってたまーにアップデートが入るのですが、クライアントは大体勝手にアップデートされるものの、 Bedrock Server は自動的にアップデートされず、手動で新しいバージョンをダウンロードして Zip ファイルを展開してバイナリファイルを置き換えてサービスを再起動して・・・とかやる必要がありました。
このプロジェクトは、一度セットアップだけやってしまえば、あとは全自動で Bedrock Server をアップデートしてくれます!ついでに Docker を使って Bedrock Server を構築するので環境を汚しません!
よろしくお願いします!
本当におしまい。