compose.yamlファイルを複数適用して異なるコンテナを作成する

docker-compose -fの落とし穴

こんにちは、ラーニングサービスの平塚です。

突然ですが、みなさんは、開発作業中にこんなことを考えた経験はありませんか?

  • 修正中のこの機能、本当に改善できているのかな? 修正前後の動作を見比べたい
  • 普段とは違うデータセットでアプリケーションの見栄えを確認したい

今回は、こういった「ちょっとした確認」がしたくなった時に使える、Docker Composeの小ネタと注意事項を紹介します。

Docker Composeとは?

本題に入る前に、DockerとDocker Composeについて簡単に紹介します。

Dockerとは、アプリケーションの実行環境をコンテナという形式でまとめる仮想化技術を指します。それぞれのコンテナは独立して動作するので、1つのマシンで複数のコンテナを同時に動かしても互いに干渉しません。
コンテナの設定情報はDockerfileに記述します。

Docker Composeとは、複数のコンテナを協調して動かすためのDockerプラグインです。たとえば、Webサーバとデータベースサーバをそれぞれ1つのコンテナとして定義して、それらを同時に実行したい時はDocker Composeを利用します。
Docker Composeの設定情報はcompose.yamlファイルに記述します。

豆知識

Docker公式は、設定ファイルの拡張子にyamlを推奨しています。拡張子ymlでも動作に違いはありません。
https://docs.docker.com/compose/intro/compose-application-model/#the-compose-file

compose.yamlファイルを複数適用する

compose.yamlファイルは一度に複数指定できます。docker-composeコマンドの-fフラグを使うと、指定した順にcompose.yamlファイルが適用され、重複する設定項目は上書きされます。
https://docs.docker.jp/compose/extends.html#extends-multiple-compose-files

たとえば、PostgreSQLのデータベースサーバとして動作するコンテナを1つ作成するcompose.yamlファイルがあったとします。

services:
  db:
    container_name: postgres
    image: postgres:17
    ports:
      - "5432:5432"
    volumes:
      - data:/var/lib/postgresql/data
    restart: always
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app-user
      POSTGRES_PASSWORD: app-pass

volumes:
  data:

ここに記載したポート番号だけを変更したコンテナを作成する時を考えましょう。まず差分を記述したcompose.override.yamlファイルを用意します。

# compose.override.yaml
services:
  db:
    ports:
      - "5433:5432"

2つのyamlファイルを適用したコンテナを作成するために、docker-composeコマンドの-fフラグを使います。土台となるyamlファイルから順番に指定してください。

docker-compose -f compose.yaml -f compose.override.yaml up -d

なお、各yamlファイルの名前や配置場所は自由です。コマンドを実行する時は、カレントディレクトリからの相対パスを指定してください。

複数のcompose.yamlファイルを使う時の注意

docker-composeコマンドの-fフラグを使うと、最小限の差分を記述するだけで複数の環境を用意できます。先ほどのPostgreSQLコンテナを作るcompose.yamlから、2つの環境を用意する場合を考えてみましょう。

# ベースとなるcompose.yaml(sampleディレクトリに配置)
services:
  db:
	container_name: postgres
	image: postgres:17
	ports:
	  - "5432:5432"
	volumes:
	  - data:/var/lib/postgresql/data
	restart: always
	environment:
	  POSTGRES_DB: app
	  POSTGRES_USER: app-user
	  POSTGRES_PASSWORD: app-pass

volumes:
  data:
  • 作成したい環境
    • 環境A
      • ポート5433で接続する
    • 環境B
      • ポート5434で接続する

まずは上書き用のcompose.yamlファイルを作成します。sampleディレクトリに、compose.a.yamlファイルとcompose.b.yamlファイルを配置しましょう。

# compose.a.yaml(環境A用の上書きcompose.yamlファイル)
services:
  db:
    ports:
      - "5433:5432"
# compose.b.yaml(環境B用の上書きcompose.yamlファイル)
services:
  db:
    ports:
      - "5434:5432"

環境A、環境Bの順番で起動します。

$ docker-compose -f compose.yaml -f compose.a.yaml up -d
[+] Running 3/3
✔ Network sample_default Created 0.1s
✔ Volume "sample_data" Created 0.0s
✔ Container postgres Started 0.3s
$ docker-compose -f compose.yaml -f compose.b.yaml up -d
[+] Running 1/1
✔ Container postgres Started 0.4s

無事に起動できたか、docker container lsコマンドで確かめてみましょう。

$ docker container ls -a
CONTAINER ID IMAGE       COMMAND                CREATED            STATUS            PORTS                                                                                    NAMES
7c4669781fcb postgres:17 "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp, 0.0.0.0:5434->5432/tcp, [::]:5434->5432/tcp postgres

コンテナが1つしかありません。POSTSに5434->5432とありますので、環境Bのコンテナしか動いていないようです。

実は、compose.a.yamlファイルとcompose.b.yamlの内容には修正すべき点が3つあります。これから順番に説明します。

注意:この記事に従って実際に操作している場合は、以下のコマンドを実行して作成済のコンテナ等を削除してください。

docker container rm -f postgres
docker volume rm sample_data
docker network rm sample_default

修正1. プロジェクト名を指定する

Docker Composeを使って起動するコンテナ群のことをプロジェクトと呼びます。Docker Composeはプロジェクト単位で環境を分離します。複数のプロジェクトを同時に起動する時は、それぞれのプロジェクトに一意な名前をつけなければなりません。

プロジェクト名は以下の優先順位に従って決まります。

  1. docker-composeコマンドの-pフラグ
  2. 環境変数COMPOSE_PROJECT_NAME
  3. compose.yamlファイルに記載されたトップレベルのname要素
    • -fフラグで複数のcompose.yamlファイルを指定した場合、最後に記載されたname要素
  4. compose.yamlファイルを収めたディレクトリ
    • -fフラグで複数のcompose.yamlファイルを指定した場合、最初に指定したcompose.yamlファイルを収めたディレクトリ名
  5. カレントディレクトリ名

https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name

今回の例では、3つのcompose.yamlファイルすべてにプロジェクト名の記述がありません。したがって、環境Aを起動する時も、環境Bを起動する時も、プロジェクト名は「ベースとなるcompose.yamlファイルが配置されたディレクトリ名」つまり「sample」です。2つの環境は同じプロジェクトだとみなされたため、環境Aの設定は後から起動した環境Bによって上書きされてしまったのです。

環境Aと環境Bが別のプロジェクトであると明示するため、上書き用compose.yamlファイルのトップレベルにname要素を追加します。

# compose.a.yaml(プロジェクト名を追加)
name: env-a
services:
  db:
  ports:
    - "5433:5432"
# compose.b.yaml(プロジェクト名を追加)
name: env-b
services:
  db:
    ports:
      - "5434:5432"

ちなみに、この段階でdocker-composeコマンドを実行すると、環境Bの起動時にエラーが発生します。

$ docker-compose -f compose.yaml -f compose.a.yaml up -d
[+] Running 3/3
✔ Network env-a_default Created 0.1s
✔ Volume "env-a_data" Created 0.0s
✔ Container postgres Started 0.3s
$ docker-compose -f compose.yaml -f compose.b.yaml up -d
[+] Running 3/3
✔ Network env-b_default Created 0.1s
✔ Volume "env-b_data" Created 0.0s
✘ Container postgres Error response from daemon: Conflict. 0.0s
Error response from daemon: Conflict. The container name "/postgres" is already in use by container "388db...". You have to remove (or rename) that container to be able to reuse that name.

どうやらコンテナ名が重複しているようです。

注意: docker-composeコマンドを実行した場合は、念のため以下のコマンドを実行してください。

docker container rm -f postgres
docker volume rm env-a_data
docker volume rm env-b_data
docker network rm env-a_default
docker network rm env-b_default

修正2. コンテナ名を上書きする

コンテナ名postgresは、ベースとなるcompose.yamlファイルのcontainer_name属性で指定されたものです。この属性はdocker container runコマンドの--nameオプションと同じ効果があり、プロジェクト名の影響を受けません。

Dockerは同じ名前を持つコンテナを複数起動できません。ベースとなるcompose.yamlファイルからcontainer_name属性を削除するか、上書き用compose.yamlファイルで別のコンテナ名を付ける必要があります。今回は後者で進めましょう。

# compose.a.yaml(コンテナ名を追加)
name: env-a
services:
  db:
    container_name: a-db
    ports:
      - "5433:5432"
# compose.b.yaml(コンテナ名を追加)
name: env-b
services:
  db:
    container_name: b-db
    ports:
      - "5434:5432"

しかしまだエラーが発生します。

$ docker-compose -f compose.yaml -f compose.a.yaml up -d
[+] Running 3/3
✔ Network env-a_default Created 0.1s
✔ Volume "env-a_data" Created 0.0s
✔ Container a-db Started 0.3s
$ docker-compose -f compose.yaml -f compose.b.yaml up -d
[+] Running 2/3
✔ Network env-b_default Created 0.1s
✔ Volume "env-b_data" Created 0.0s
⠙ Container b-db Starting 0.1s
Error response from daemon: driver failed programming external connectivity on endpoint b-db (24c42...): Bind for 0.0.0.0:5432 failed: port is already allocated

今度はポート5432が重複しているようです。最後にここを直しましょう。

注意: docker-composeコマンドを実行した場合は、念のため以下のコマンドを実行してください。

docker container rm -f a-db
docker container rm -f b-db
docker volume rm env-a_data
docker volume rm env-b_data
docker network rm env-a_default
docker network rm env-b_default

修正3. 属性を強制的に上書きする

基本的に、compose.yamlファイルの内容は、後から指定したcompose.yamlファイルの内容に上書きされます。ただし、ports, expose,
external_linksといった複数の値を持つ属性は、-fフラグで指定したすべてのcompose.yamlファイルの内容を結合します。
https://docs.docker.com/reference/compose-file/merge

環境Aと環境Bは、それぞれの上書き用compose.yamlファイルに記載したポート(5433または5434)に加えて、ベースとなるcompose.yamlファイルに記載したポート(5432)も開放しようとします。しかし、環境Bの起動時には環境Aがすでにポート5432を使っているためエラーが発生します。

結合ではなく上書きする時は、属性名の隣に!overrideキーワードを追加します。ただし、このキーワードはDocker Composeのバージョン2.24.4以上でしか使えません。
https://docs.docker.com/reference/compose-file/merge/#replace-value

# compose.a.yaml(!overrideキーワードを追加)
name: env-a
services:
  db:
    container_name: a-db
    ports: !override
      - "5433:5432"
# compose.b.yaml(!overrideキーワードを追加)
name: env-b
services:
  db:
    container_name: b-db
    ports: !override
      - "5434:5432"

ついにエラーがなくなりました。

$ docker-compose -f compose.yaml -f compose.a.yaml up -d
[+] Running 3/3
✔ Network env-a_default Created 0.1s
✔ Volume "env-a_data" Created 0.0s
✔ Container a-db Started 0.3s
$ docker-compose -f compose.yaml -f compose.b.yaml up -d
[+] Running 3/3
✔ Network env-b_default Created 0.1s
✔ Volume "env-b_data" Created 0.0s
✔ Container b-db Started 0.3s

docker container lsコマンドで確かめると、2つのコンテナが同時に実行されていることが分かります。

$ docker container ls -a
CONTAINER ID IMAGE       COMMAND                CREATED            STATUS            PORTS                                       NAMES
a3465ef5d837 postgres:17 "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5434->5432/tcp, [::]:5434->5432/tcp b-db
e6011ef117bd postgres:17 "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5433->5432/tcp, [::]:5433->5432/tcp a-db

後片付け

以下のコマンドで作成したコンテナ等を削除してください。

docker container rm -f a-db
docker container rm -f b-db
docker volume rm env-a_data
docker volume rm env-b_data
docker network rm env-a_default
docker network rm env-b_default

まとめ

docker-composeコマンドは、-fフラグを使うと複数のcompose.yamlファイルを指定できます。ただし、複数の環境を同時に実行したい時は、以下の3つに注意しましょう。

  • プロジェクト名を明示する
  • container_name属性は必ず上書きする
  • 属性がマージされる時の挙動を意識する
    • 強制的に上書きしたい時は!overrideキーワードを使う

研修コース紹介

カサレアルでは、Dockerをはじめとしたインフラ系の知識を学べるコースを開催しています。詳細や開催日程は以下のリンクを御覧ください。
https://www.casareal.co.jp/ls/service/openseminar/search/cloudinfra

また、カサレアルでは、座学形式の講義のほかにも、一社様向けに課題解決型(Problem Based Learning, PBL)の研修サービスも提供しています。
講義でインプットした知識を、実際の開発演習を通じてアウトプットすることで、理解の深化と知識の定着を図ることができます。
さらにチームごとに開発したい機能を設計し、開発からテストまで一貫して取り組むことで、プロダクト開発にまつわる知識の定着や深堀り、また困った時の調べ方、助けの求め方も学べるとご好評いただいております。

今回の記事は、演習をサポートする立場として「受講者の手元にあるソースコードは極力変更せずに、全チームの成果物を同時に起動したい」というニーズを満たすべく試行錯誤した結果をまとめました。

チーム開発演習の規模や採用技術は柔軟に対応いたします。詳細についてはお気軽にお問い合わせください。

--------------------------
開発支援・技術研修のご要望・ご相談はこちらから
--------------------------
【この技術ブログを読んだエンジニアの皆様へ】
カサレアルブログをお読みいただき、ありがとうございます!

私たちは、常に新しい技術に挑戦し、ユーザーのニーズに応えるサービスを提供しています。
もし、当社の技術への情熱や、会社・チーム・社員の雰囲気に共感いただけたなら、
ぜひ私たちと一緒に働きませんか?
現在、株式会社カサレアルでは事業拡大に伴い、新たな仲間となるエンジニアを積極的に募集しています。

少しでも興味をお持ちいただけましたら、まずは弊社のことを知っていただけると嬉しいです。
▼採用サイト
https://www.casareal.co.jp/recruit/career
▼社員インタビュー
https://hrmos.co/pages/casareal/jobs/0000016
▼エンジニアの仲間になる! エントリーはこちらから
https://hrmos.co/pages/casareal/jobs

皆様のエントリーを心よりお待ちしています!

~PMO 第2弾~ 大規模なシステム改修で複数の開発ベンダーが関わるプロジェクトの円滑な進行は!

コメントを残す

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

コメント ※

名前 ※

メール ※

サイト