DockerとDocker Composeで定義する環境変数がコンテナ内でどのような値になるかの比較

# はじめに

DockerやDocker Composeでコンテナを起動する際に、コンテナ内で使う環境変数を定義することができます。例えばDockerコンテナを起動するコマンドで、環境変数と値を引数で指定したり、環境変数を記載したファイルを引数で指定したりすることで実現できます。
ここで注意が必要なのが、環境変数の指定方法によってコンテナ内で設定される値が変わってしまうことです。例えば``VALUE1="value1"``のようにダブルクォートした環境変数を設定してコンテナを起動すると、渡し方よっては文字列にダブルクォートが残ったままになります。その結果、環境変数を参照して実行するプログラムが上手く動かない事態が起きたりします。

そこで今回は、環境変数の渡し方でDockerコンテナ内でどのように値が変わるのかを比較してみました。

使ったDockerとDocker Composeのバージョンは以下です。


```
$ docker --version
Docker version 27.0.3, build 7d4bcd8
```

```
$ docker compose version
Docker Compose version v2.28.1
```

# 事前準備

ホストOSのターミナルで、以下の２つの環境変数を定義します。
```bash
export VAR_HOST_OS_EXP='host_os_exp'
VAR_HOST_OS_LOCAL='host_os_local'
```

Dockerコンテナ起動時は次のような環境変数を定義し、値にホストOSで定義した環境変数を含むようにします。また、起動時に定義した環境変数の値を別の環境変数で参照可能かを確認するために``VAR1``,``VAR2``を追加しています。
```bash
UNQUOTED=$VAR_HOST_OS_EXP
DOUBLE_QUOTED="$VAR_HOST_OS_EXP"
SINGLE_QUOTED='$VAR_HOST_OS_EXP'
UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL
VAR1=hello world # comment
VAR2=$VAR1
```

Dockerコンテナを起動した際に、上記の環境変数の指定方法によって値がどのように変わるのかをこれから比較していきます。

# 試す前に一言

これから以下の４つのケースでコンテナ内の環境変数に値がどのように格納されるかを試していきます。

* (1) ``docker run``で、``--env`` で環境変数を指定
* (2) ``docker run``で、``--env-file`` で環境変数ファイルを指定
* (3) Docker ComposeのYAMLに環境変数を指定
* (4) Docker ComposeのYAMLに環境変数ファイルを指定

上記４ケースの違いがひと目でわかるようにここで表にまとめようと思ったのですが、出力がかなりケースバイケースであることがわかり断念しました。

試してみてわかったのは、**各ケースでの出力結果がシェルまたはYAMLの規則に従うことが多い**ということです。なので、シェルとYAMLでの値の格納方法について理解を深めておくと行き詰まりにくくなるはずです。

# 各ケースで比較

## (1) docker runで、 --env で環境変数を指定する場合


```
$ docker run --rm \
--env UNQUOTED=$VAR_HOST_OS_EXP \
--env DOUBLE_QUOTED="$VAR_HOST_OS_EXP" \
--env SINGLE_QUOTED='$VAR_HOST_OS_EXP' \
--env UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL \
--env VAR1='hello world' \
--env VAR2=$VAR1 \
debian:12-slim env | grep -e QUOTE -e VAR
(出力結果)
UNQUOTED=host_os_exp
DOUBLE_QUOTED=host_os_exp
SINGLE_QUOTED=$VAR_HOST_OS_EXP
UNQUOTED_LOCAL=host_os_local
VAR1=hello world
VAR2=
```

※ VAR1の指定に関して、``--env VAR1=hello world``とすると実行エラーになるのは自明なのでシングルクォートしています。

### 特徴
* ダブルクォート、シングルクォートの両方が外される
* ホストOSの環境変数の値
    * exportしなくても反映される
    * シングルクォートの場合は反映されない

シェルの場合、ダブルクォートした環境変数は値が出力されますが、シングルクォートの場合は値ではなく文字列がそのまま出力されます。上で``-env``で定義した環境変数も同じように出力されており、**シェルの規則に従って環境変数の値を指定すればよさそう**です。

※シェルのクォートを使った場合の挙動は[こちら](https://qiita.com/ko1nksm/items/60b67cb24aa4ae634dd5)の記事等が参考になります。

## (2) docker runで、 --env-file で環境変数ファイルを指定する場合

以下のように用意した環境変数ファイル``.env``を、docker runで指定して実行します。

```sh:.env
UNQUOTED=$VAR_HOST_OS_EXP
DOUBLE_QUOTED="$VAR_HOST_OS_EXP"
SINGLE_QUOTED='$VAR_HOST_OS_EXP'
UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL
VAR1=hello world　# comment
VAR2=$VAR1
```

```
$ docker run --rm --env-file .env debian:12-slim env | grep -e QUOTED -e VAR
UNQUOTED=$VAR_HOST_OS_EXP
DOUBLE_QUOTED="$VAR_HOST_OS_EXP"
SINGLE_QUOTED='$VAR_HOST_OS_EXP'
UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL
(出力結果)
UNQUOTED=$VAR_HOST_OS_EXP
DOUBLE_QUOTED="$VAR_HOST_OS_EXP"
SINGLE_QUOTED='$VAR_HOST_OS_EXP'
UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL
VAR1=hello world # comment
VAR2=$VAR1
```

### 特徴
* ダブルクォート、シングルクォートは外れずに残る
* ホストOSの環境変数の値は反映されない
* ``.env``内の環境変数の値も、別の環境変数を使って反映することができない
* 値にスペースが入っていても、``=``の右側に記載した全ての文字列が値として反映される
* 値の後ろに#をつけてコメントアウトしたつもりでも、それがそのまま文字列として反映されてしまうので注意

試したケース全てで、**``.env``に書いた値がそのまま反映される**ことがわかりました。値に別の環境変数を含めたり、値の後ろにコメントアウトも出来ないため、かなり制限された使い方しかできな印象です。

## (3) Docker ComposeのYAMLに環境変数を指定する場合

Docker Compose用のYAMLファイルで、``environment``セクションに環境変数を設定するケースです。次のような２つの定義方法があります。

* 3-1) ``- VAR=value``のようにリストの要素として定義
* 3-2) ``VAR: value``のようにKeyValueで定義

### 3-1) ``- VAR=value``と定義するケース

次のように、Docker Compose用のYAMLファイルで環境変数をリストの要素として定義して、``docker compose``コマンドを実行します。

```yaml:compose_env1.yaml
services:
  output_env:
    image: debian:12-slim
    environment:
      - UNQUOTED=$VAR_HOST_OS_EXP
      - DOUBLE_QUOTED="$VAR_HOST_OS_EXP"
      - SINGLE_QUOTED='$VAR_HOST_OS_EXP'
      - UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL
      - VAR1=hello world # comment
      - VAR2=$VAR1
    entrypoint:
      ["sh", "-c", "env"]
```

```
$ docker compose -f compose-env1.yaml up 2>&1 \
| grep -e QUOTE -e VAR \
| grep -v warning
(出力結果)
output_env-1  | UNQUOTED=host_os_exp
output_env-1  | DOUBLE_QUOTED="host_os_exp"
output_env-1  | SINGLE_QUOTED='host_os_exp'
output_env-1  | UNQUOTED_LOCAL=
output_env-1  | VAR1=hello world
output_env-1  | VAR2=hello world
```

#### 特徴
* ダブルクォート、シングルクォートは外れずに残る
* ホストOSの環境変数の値はexportしていると反映される
* コンテナ起動時に定義した環境変数の値を別の環境変数で参照可能
* 値にスペースが入っていても、その後ろの文字列含めて値として反映される
* ただし、#でコメントアウトしたものは無視される

docker runで ``--env-file``を指定するケースよりも意図した出力になるものが多く、使い勝手が良さそうです。

#### 本当は環境変数定義全体をクォートした方がいい

上の方法で環境変数を定義するやり方は実はベストではないです。なぜならば使う文字によっては、**YAMLの規則によって値が変わってしまうことがある**ためです。

例えば環境変数の値に``:``（コロン）が含まれる場合に``environment``の要素に以下のように書いたら、``docker compose``で起動する際にどのケースでも構文エラーになってしまいます。

```yaml
- VAR1=hello: world
- VAR1='hello: world'
- VAR1="hello: world"
```

これは``:``がYAMLだと特別な意味を持つ記号で、YAMLをパースする際に文字とは見なされずに処理されてしまうためです。
そこで以下のように要素全体をシングルクォートまたはダブルクォートすると、``:``を文字として認識してくれます。

```yaml
- 'VAR1=hello: world' # または以下
- "VAR1=hello: world"
```

「YAMLにとって特別な記号」は ``:``以外にも``{``や``|``などのようにいくつかあり、文字として使いたい場合にそのまま使うと予期せぬ動作を引き起こしてしまう可能性があります。そこで、以下のように常に要素全体をクォートしていた方が良さそうです。

```yaml:compose-env1-improve.yaml
services:
  output_env:
    image: debian:12-slim
    environment:
      - 'UNQUOTED=$VAR_HOST_OS_EXP'
      - 'DOUBLE_QUOTED="$VAR_HOST_OS_EXP"'
      - "SINGLE_QUOTED='$VAR_HOST_OS_EXP'"
      - 'UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL'
      - 'VAR1=hello world' # comment
      - 'VAR2=$VAR1'
    entrypoint:
      ["sh", "-c", "env"]
```

上記の出力結果は、3-1)の最初に示した「クォートしないケース」と同じになります。

### 3-2) ``VAR: value``と定義するケース

先ほど示したDocker Compose用のYAMLで、``environment``での環境変数の定義方法を変えて、``VAR: value``の辞書型の形式で指定します。

```yaml:compose_env2.yaml
services:
  output_env:
    image: debian:12-slim
    environment:
      UNQUOTED: $VAR_HOST_OS_EXP
      DOUBLE_QUOTED: "$VAR_HOST_OS_EXP"
      SINGLE_QUOTED: '$VAR_HOST_OS_EXP'
      UNQUOTED_LOCAL: $VAR_HOST_OS_LOCAL
      VAR1: hello world # comment
      VAR2: $VAR1
    entrypoint:
      ["sh", "-c", "env"]
```

以下が実行結果です。

```
$ docker compose -f compose-env2.yaml up 2>&1 | \
grep -e QUOTE -e VAR | \
grep -v warning
(出力結果)
output_env-1  | UNQUOTED=host_os_exp
output_env-1  | DOUBLE_QUOTED=host_os_exp
output_env-1  | SINGLE_QUOTED=host_os_exp
output_env-1  | UNQUOTED_LOCAL=
output_env-1  | VAR1=hello world
output_env-1  | VAR2=hello world
```

#### 特徴

シングルクォートとダブルクォートが外れるのを除くと、3-1)の``- VAR=value``で定義する形式と同じです。

* ダブルクォート、シングルクォートは**外れる**
* ホストOSの環境変数の値はexportしていると反映される
* コンテナ起動時に定義した環境変数の値を別の環境変数で参照可能
* 値にスペースが入っていても、その後ろの文字列含めて値として反映される
* ただし、#でコメントアウトしたものは無視される

このケースでも文字列に``:``等が含まれる場合はクォートしないと予期せぬ結果になる可能性があり、注意が必要です。少なくとも文字列を直接書く場合は、以下のようにクォートするのが無難でしょう。

```yaml
VAR1: 'hello world' # または以下
VAR1: "hello world"
```


## (4) Docker ComposeのYAMLに環境変数ファイルを指定する場合

最後は環境変数を定義したファイルを別に用意して、Docker ComposeのYAMLには環境変数ファイルを指定する例です。
ケース2で``docker run``する際に使ったのと同じ環境変数ファイルを以下のように用意します。

```sh:.env
UNQUOTED=$VAR_HOST_OS_EXP
DOUBLE_QUOTED="$VAR_HOST_OS_EXP"
SINGLE_QUOTED='$VAR_HOST_OS_EXP'
UNQUOTED_LOCAL=$VAR_HOST_OS_LOCAL
VAR1=hello world　# comment
VAR2=$VAR1
```

Docker ComposeのYAMLには、上記の``.env``を環境変数ファイルとして参照するように書きます。

```yaml:compose-env-file.yaml
services:
  output_env:
    image: debian:12-slim
    env_file:
      - ".env"
    entrypoint:
      ["sh", "-c", "env"]
```

実行コマンドと結果は以下です。

```
$ docker compose -f compose-env-file.yaml up 2>&1 | \
grep -e QUOTE -e VAR | \
grep -v warning
(出力結果)
output_env-1  | UNQUOTED=host_os_exp
output_env-1  | DOUBLE_QUOTED=host_os_exp
output_env-1  | SINGLE_QUOTED=$VAR_HOST_OS_EXP
output_env-1  | UNQUOTED_LOCAL=
output_env-1  | VAR1=hello world
output_env-1  | VAR2=hello world
```

#### 特徴

* ダブルクォートは外れる
* シングルクォートされた環境変数はそのまま出力される
* ホストOSの環境変数の値はexportしていると反映される
* コンテナ起動時に定義した環境変数の値を別の環境変数で参照可能
* 値にスペースが入っていても、その後ろの文字列含めて値として反映される
* ただし、#でコメントアウトしたものは無視される

環境変数はYAMLで指定しているわけではないので、ケース(3)で気にする必要があった**YAMLの規則は気にしなくてもOK**です。なので例えば、以下のように文字列に``:``が含まれていてもクォート等の処置は不要です。

```sh:.envの一部
VAR1=hello: world # OK　
```

また、環境変数を値として使う場合に、シングルクォートをすると環境変数の文字列がそのまま出力される特徴もあります。これより、**シェルの規則が適用されている**可能性が高そうです。

一方で、例えばシェルでクォート無しでスペースを含む文字列を定義すると、２番目の文字列は別のコマンドと見なされてエラーになってしまいます。

```bash
$ VAR=hello world
-bash: world: コマンドが見つかりません
```

これがDocker Composeで環境変数ファイルを指定した場合だと、スペースの後の`` world``まで一つの文字列と見なされるので、完全にシェルの規則に従っているわけでもなさそうです。

# まとめ

最後に、それぞれのケースの特徴をやや抽象的に表にまとめてみます。

| Index | ツール | 変数定義方法 | シェルの規則に従う？ | YAMLの規則に従う？ |
| --- | --- | --- | --- | --- |
| 1 | Docker | 引数(``--env``) | ◯ | ✗ |
| 2 | Docker | ファイル(``--env-file``) | ✗ | ✗ |
| 3 | Docker Compose | パラメータ(``environment:``) | ✗ | ◯ |
| 4 | Docker Compose | ファイル(``env_file:``) | △ | ✗ |