マルチステージビルドなDockerfileのARGでやらかしがちな件について

# TL;DR

マルチステージビルドな Dockerfile で ARG を使う場合は、スコープがステージ内に閉じていることに注意が必要です！ちゃんと各ステージごとに使う ARG を宣言しましょう！
また、複数のステージで使う ARG にデフォルト値があるのなら、グローバルな ARG を定義してそこにデフォルト値を記載しましょう！

こんな感じで作ればオッケーです！

```dockerfile:Dockerfile
# 各ステージで共通して利用する 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 の値を参照できていないことに気づいて初めて知りました。

[こちらのコメント](https://github.com/moby/moby/issues/37345#issuecomment-400245466)によれば、

> 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（最初のビルドステージより前で定義する)を使えば、ビルドステージごとにその値を利用することができます。

とのことです。マジか。

[公式ドキュメント](https://docs.docker.jp/engine/reference/builder.html#scope)にもしっかり載ってましたわ。

> ARG 命令の変数スコープは、それが定義されたビルドステージが終了するときまでです。 複数のビルドステージにおいて ARG を利用する場合は、個々に ARG 命令を指定する必要があります。

最初にドキュメント読んだときは、ビルドステージの意味も分からないまま「あーそーゆーことね完全に理解した」と思ったような気が。。。

# ARG の動きを見てみる

では実際にマルチステージビルドでの ARG の動きを見ていきましょう。

## 本稿の動作確認環境

本稿の執筆にあたっては、下記のバージョンで動作を確認しています。

```shell-session
# docker --version
Docker version 18.09.1, build 4c52b90
```

## Dockerfile

今回の検証で使う Dockerfile は次の通りです。

```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` による値の指定を行わずにビルドしてみます。

```shell-session
# 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` による値の指定ありでビルドしてみます。

```shell-session
# 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 のデフォルト値](https://docs.docker.jp/engine/reference/builder.html#default-values)で説明されている内容と一致しますね。 `--build-arg` で指定された値はグローバルなデフォルト値及びステージ内のデフォルト値のどちらよりも優先されます。
*   third_stage ではまたもやすべての値が表示されていません。 `--build-arg` で値が指定されていても、そのステージ内で宣言されていなければやはり利用できないようです。

## まとめ

*   マルチステージビルドな Dockerfile で ARG を利用する際には、すべてのステージでそれぞれ利用する ARG を宣言する必要がある。
*   複数のステージで利用している ARG のデフォルト値を定義したければ、グローバルな ARG を定義(最初の FROM よりも前に定義)してそこにデフォルト値を記載すると良い。
*   ARG の値の優先度は
    1.  `--build-arg` での指定値 [優先度高]
    2.  ステージ内のデフォルト値
    3.  グローバルなデフォルト値 [優先度低]

おしまい。

## 宣伝

マルチステージビルドの ARG の挙動に気づくきっかけとなったプロジェクトの宣伝です。

*   [docker-minecraft-bedrock](https://github.com/ktooi/docker-minecraft-bedrock)

Minecraft の Bedrock Server を楽に運用しようぜ！という趣旨のプロジェクトです。
Minecraft ってたまーにアップデートが入るのですが、クライアントは大体勝手にアップデートされるものの、 Bedrock Server は自動的にアップデートされず、手動で新しいバージョンをダウンロードして Zip ファイルを展開してバイナリファイルを置き換えてサービスを再起動して・・・とかやる必要がありました。
このプロジェクトは、一度セットアップだけやってしまえば、あとは全自動で Bedrock Server をアップデートしてくれます！ついでに Docker を使って Bedrock Server を構築するので環境を汚しません！

よろしくお願いします！

本当におしまい。