[環境別デプロイ] docker compose -f の挙動について調べてみた。

# はじめに
docker composeを使ってステージング環境別にデプロイするとき、composeファイルの内容を環境ごとに少し変えたい場合があります。しかし新しいcomposeファイルを一から開発用、ステージング用、本番用…と作成してしまうと、重複部分が二重定義となり、変更が発生した際に面倒になります。

[docker公式](https://docs.docker.jp/compose/production.html) としては、以下のように、-f オプションを使用して各環境の差分だけをproduction.yaml等に記述するように勧めています。

>production.yml のような追加 Compose ファイルを使い、プロダクションに相応しい設定を定義したくなるでしょう。この設定ファイルには、元になった Compose ファイルからの変更点のみ記述できます。追加の Compose ファイルは、元の docker-compose.yml の設定を上書きする新しい設定を指定できます。

このドキュメントにある、" 元の docker-compose.yml の設定を上書きする新しい設定を指定 "という文言について、同じボリュームパスを指定したり、.envファイルを追加読取りした場合に、「後で定義したものだけが適用されるか」「両方適用されるか」の挙動が詳しく分からなかったため、軽く調査しました。

# 調査

軽量なalpine環境を用いて、以下のようなボリュームファイル、envファイルを用いてテストしました。

```:ディレクトリ構成
docker-compose-f-test/
| -- app/
    | -- compose_volume
| -- app_prod
    | -- prodction_volume
| -- .env
| -- .prod.env
| -- docker-compose.yaml
| -- production.yaml
```
なお、ここでのcompose_volumeとprodction_volumeはマウントを調べるためだけのファイルなので意味の無い空ファイルです。

```:.env
APP_ENV=dev
APP_NAME=DOCKER_COMPOSE_TEST
```
```:.prod.env
APP_ENV=prod
```
```:docker-compose.yaml
version: "3.0"
services:
  alpine:
    image: alpine
    volumes:
      - ./app:/app
      - ./app:/app_prod
    env_file:
      - .env
```
```:production.yaml
services:
  python:
    volumes:
      - ./app_prod:/app_prod
    env_file:
      - .prod.env

```

コンテナ内の/app_prodについては、あえてdocker-compose.yamlとproduction.yamlでローカルの同じディレクトリをマウントし、どちらが優先されるかを確認します。

この状態でdockerを立ち上げて、envとディレクトリ構成を確認してみます。
```
$ docker compose -f docker-compose.yaml -f production.yaml run --rm alpine sh -c "ls app_prod && ls && env"

# 結果 (抜粋)
production_volume

app       app_prod

APP_NAME=DOCKER_COMPOSE_TEST
APP_ENV=prod
```

# 本番環境ではマウントを止めたいとき
開発環境ではバインドボリュームを使用してローカルでコードを編集してコンテナにそのまま反映ということをしつつ、本番環境ではバインドボリュームを止めたいときがあります。ローカルのコードが変わるだけで本番に影響が出てしまうのは怖いですからね。そんな時は以下の用に記述します。

```:production.yaml
services:
  python:
    volumes:
      - /app
```
volume設定は普通はローカルパス:コンテナ内パス、としますが、volumeしたくない場合はコンテナ内パスのみだけを記述します。

このようにした上で、以下のようにコンテナ内のapp/を見てみると、空になっていることが分かります。

```
$ docker compose -f docker-compose.yaml -f production.yaml run --rm alpine sh -c "ls app -a"

# 結果 (抜粋)
. ..
```

ちなみに、この技を使用することで、ディレクトリ全体を再帰的にマウントしつつ、特定の一部のディレクトリだけマウントしないようにすることも可能です。
（普段react環境をdockerで作ることが多いのですが、node_modulesをバインドマウントするとホットリロードが遅くなってしまうため、同じようにしてnode_modulesのみバインドマウントから外し、ボリュームマウントにするようにしています。）

docker-compose.yamlで定義したvolume, envから追加される形でproduction.yamlのvolume, envが加わりました。envで同じ環境変数（APP_ENV）をセットした場合は、-fで後に適用したproductionで指定した.prod.envの"APP_ENV=prod"が適用されていて、.prod.envで何も指定しなかったAPP_NAMEに関しても、生きています。
あえてdocker-compose.yamlとproduction.yamlでローカルの同じディレクトリをマウントしたところも、-fで後に適用してproductionが優先されています。

# 結論
結論としてはyaml記述としてリスト記述のものは「両方適用されつつ、被った場合は後のものが適用される」というものでした。リスト記述でないもの（restart）等は、後に指定したファイルがそのまま適用されます。考えてみれば当たり前でそりゃそうだろという感じですが、想定と違う動きをされてしまうと本番にモロ影響が出てしまう内容なので検証出来てよかったです。


# 何に役立つか
- 環境別に、異なる環境変数を用意したり、ポートマッピングを変更したい場合。
- 本番用環境はバインドボリュームを使用したくない場合（ローカルのコードが書き換わるとコンテナ内のコードも書き換わってしまうのは怖いとき）
- ログ収集方法を変える場合（EC2にDockerを置き、Cloudwatchにlogを出力する場合等。逆にEC2以外ではcomposeファイルにlog設定が書かれていると、立ち上がらなくなってしまうのを防止）
...等。

# 余談
- docker compose config
調査中に分かったことですが、`docker compose config`コマンドを使えば、-fで複数の設定をマージしたcomposeファイルを出力できるので、これを使えばいちいちコンテナを作る必要はありませんでした。
以下のようにコマンドを打てば、どんな設定かが分かります。
```
$ docker compose -f docker-compose.yaml -f production.yaml config
```

- compose設定のリスト項目の更新
volume以外のリスト設定（envやports）は、調べた限りマージしかできず、除外はできないようでした。
なので例えば、開発環境でポートマッピングを80と443でしていて、本番環境では80のマッピングを除外するようなことはできないようです。それをしたい場合は、開発環境用のdocker composeのyamlを新たに作らないので、composeファイルが増えるのが少し面倒ですね。

...