Docker Compose 複数ファイルの読み込みとマージ
compose.override.yamlが単独で読み込まれる条件、ファイル順序によるマージの実際、ポートが開いたままになるportsの落とし穴、devとprodを分けるincludeの使い方を解説します。
複数のファイルがある場合の Compose の動作
Docker Compose では、複数のファイルから1つのプロジェクトを構築できます。ファイルを指定された順序で読み込み、1つのモデルにマージします。そのため、値が競合する場合は後のファイルの値が優先されます。コマンドラインからこの処理を行う方法は2つあります。Compose が自動的に読み込む override ファイルと、手動で指定する -f フラグです。3つ目の方法はファイル内の include 要素で、これら2つとは動作が異なります。
マージは単純な上書きではありません。マッピングはキー単位でマージされ、シーケンスは追加され、一部のフィールドは全体が置き換えられます。この違いが予期しない動作の原因になります。特に、ほとんどの利用者が問題に遭遇するのが ports リストです。
以下では、旧 docker-compose スクリプトではなく docker compose plugin を使用する Compose v2 を前提とします。確認するには docker compose version を実行します。まだ Compose ファイルを作成していない場合は、Docker Compose の基本ガイドから始めてください。
Composeが指定なしで読み込むoverrideファイル
-fフラグなしでdocker compose upを実行すると、Composeは作業ディレクトリとその親ディレクトリを検索し、compose.yamlまたはdocker-compose.yamlを探します。overrideファイルがベースファイルと同じ場所にある場合、Composeはそれも自動的に読み込みます。
ls compose.yaml compose.override.yaml
docker compose up -d両方のファイルが存在する場合、手動で両方を指定した場合と同じ結果になります。
docker compose -f compose.yaml -f compose.override.yaml up -dComposeが認識する名前はcompose.override.yaml、compose.override.yml、および旧形式のdocker-compose.override.yml、docker-compose.override.yamlです。たとえばcompose.dev.yamlのような別の名前は、-fで指定した場合にだけ読み込まれます。
-fを1つでも指定すると、自動読み込みは停止します。docker compose -f compose.yaml upはそのファイルだけを正確に読み込み、overrideファイルを無視します。この動作を利用して、このガイドでは後ほどdev環境とprod環境のパターンを構成します。
サーバーでは、この動作に注意が必要です。デプロイディレクトリに残ったoverrideファイルは、そのディレクトリから実行するすべての引数なしのdocker composeコマンドで読み込まれます。cronジョブが実行するコマンドも対象です。その結果、本番スタックが、配布するつもりのなかったソースディレクトリをbind mountすることがあります。デプロイ後はdocker compose configを実行し、出力内容を確認してください。
-f による順序付けと相対パスの解決場所
Compose は、指定した順序でファイルから設定を構築します。後のファイルは、前のファイルの設定を上書きし、設定を追加します。左から右へ適用され、最後の設定が優先されます。
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dプロジェクト内のすべてのコマンドで、同じファイル一覧を使用する必要があります。2つのファイルを指定して up を実行し、1つのファイルを指定して logs を実行すると、異なるマージ済みモデルを操作することになります。その結果、Compose が存在しないと報告するサービスを作成しやすくなります。代わりに、COMPOSE_FILE 環境変数でファイル一覧を一度だけ設定してください。
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dLinux では区切り文字は : です。COMPOSE_PATH_SEPARATOR で区切り文字を変更できます。COMPOSE_FILE はプロジェクトの .env ファイルにも記述できます。これにより、シェルの履歴ではなくチェックアウトの一部として管理できます。コマンドラインで明示的に設定した値は、環境変数より優先されます。
次に、bind mount で問題になりやすいルールを説明します。-f で複数のファイルを使用すると、すべてのファイル内の相対パスは、それを記述したファイルではなく、最初のファイルがあるディレクトリを基準に解決されます。deploy/prod/compose.prod.yaml 内に ./data:/var/lib/postgresql/data と記述しても、Compose はベースファイルと同じ場所に ./data を探します。Docker はその誤ったパスに空のディレクトリを作成し、コンテナは中身のない状態で起動します。これはデータ損失のように見えますが、実際にはデータ損失ではありません。--project-directory を渡してベースパスを自分で設定するか、include を使用してください。後者では、各ファイルをそれぞれのディレクトリを基準に解決します。
プロジェクト名も同じベースディレクトリから決まるため、最初に指定するファイルを変更するとプロジェクト名が変わることがあります。プロジェクト名が変わると、新しいコンテナ名とボリューム名が作成されます。古いボリュームは、以前の名前でディスク上に残ります。ベースファイルのトップレベルに name: を設定して、プロジェクト名を固定してください。
name: myappマージされるフィールドと置き換えられるフィールド
Composeはフィールド名ではなく、値の型に基づいてマージします。
- 単一値フィールドは置き換えられます。
image、command、entrypoint、mem_limitは後の値で完全に置き換えられます。commandに引数を追加することはできません。上書きによって行全体が書き換えられるためです。 - マッピングはキー単位でマージされます。
environment、labels、volumes、devicesでは、両方のファイルのすべてのキーが保持されます。両方に存在するキーは、後のファイルの値が優先されます。environmentとlabelsでは、キーは変数名またはラベル名です。volumesとdevicesでは、キーはコンテナパスです。 - シーケンスは追加されます。
dns、dns_search、expose、tmpfs、external_linksは連結されます。expose: ["3000"]を含むベース設定と["4000", "5000"]を含む上書き設定をマージすると、["3000", "4000", "5000"]になります。
4つのシーケンスには識別キーがあります。そのため、そのキーが一致するエントリは追加ではなくマージされます。volumes、secrets、configsはtargetで照合されます。portsはip、target、published、protocolの組み合わせで照合されます。
このportsのルールは、注意点なので2回確認してください。2つのポートエントリが同一のエントリと見なされるのは、4つの部分がすべて一致する場合だけです。いずれか1つを変更すると、Composeは別の無関係なポートとして認識するため、両方を保持します。
override後もポートが公開される理由
すべてのインターフェースでサービスを公開するベースファイルです。
services:
web:
image: nginx:1.27
ports:
- "8080:80"リバースプロキシを前段に置くため、localhostのみにバインドするよう記述したoverrideです。
services:
web:
ports:
- "127.0.0.1:8080:80"動作したと判断する前に、結果を確認してください。
docker compose -f compose.yaml -f compose.prod.yaml config出力には両方のエントリが含まれています。ip の部分が異なり、0.0.0.0 と 127.0.0.1 になっているため、マージ処理では別のポートとして扱われます。そのため、削除しようとしたパブリックバインドが、モデルに残っています。これは他の環境よりDockerで重要です。公開ポートは、ファイアウォールルールより先にiptablesへ書き込まれるためです。この仕組みについては、公開されたDockerポートがufwを通過する理由で説明しています。
修正方法は2つあります。明示的な方法は !override タグです。このタグは属性全体を置き換え、マージ規則を適用しません。
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override にはCompose v2.24.4以降が必要です。移植性の高い方法では、タグは必要ありません。ベースファイルから ports を完全に除外し、環境固有のファイルでのみ宣言します。マージするものがなければ、漏洩するものもありません。以下の実例では、このパターンを使用しています。
ベースファイルで設定された値を削除する
!reset は属性を削除し、デフォルト値または null に戻します。値を受け取りますが無視するため、空の有効な値を指定します。
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset には Compose v2.24 以降が必要です。ベースファイルを編集できない場合、たとえばベンダーのフラグメントを取り込む場合に使用します。
部品から構成するスタック用の include
include は、別の Compose アプリケーションをモデルに取り込みます。これはトップレベル要素であり、フラグではありません。
include:
- path: ../commons/compose.yamlinclude の各パスは、それぞれ独自のプロジェクトディレクトリを持つ個別の Compose アプリケーションモデルとして読み込まれます。そのため、そのファイル内の相対パスは、各ファイル自身のディレクトリを基準に解決されます。これが -f との実際の違いです。フラグメントが別のフォルダーや別のリポジトリにある場合に include が適している理由でもあります。
長形式ではサブオプションを指定できます。
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath にはリストを指定できます。指定したファイルは通常のルールで結合されてから、モデルに追加されます。project_directory は、include するファイル内の相対パスを解決するための基準パスを設定します。env_file は、include するファイルに独自の変数を指定して補間できるようにします。これにより、共有フラグメントがプロジェクトの .env を暗黙的に読み取ることを防げます。include には Compose v2.20.0 以降が必要です。
ファイルと include するファイルの間でリソース名が重複すると、暗黙的にマージされず、エラーとして報告されます。これは意図された動作です。include するファイルで宣言された内容を変更するには、変更を compose.override.yaml に記述します。オーバーライドは組み立て後のモデルに適用されるため、include されたリソースに競合せず変更を加えられます。
要点は次のとおりです。include は個別のアプリケーションを構成し、-f は1つのアプリケーションに設定を重ねます。
1台のVPSで開発環境と本番環境を分離する
この構成全体を3つのファイルで示します。ベースファイルでは、すべての環境で共通する設定を宣言します。ポートは一切公開しません。
name: myapp
services:
app:
image: ghcr.io/example/app:1.4.2
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
LOG_LEVEL: info
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db_data:depends_on 条件により、アプリケーションは単に存在するコンテナではなく、応答するデータベースを待機します。詳しくは healthchecksとdepends_on条件 を参照してください。POSTGRES_PASSWORD はプロジェクトの .env ファイルから補間されます。このファイルはgitに含めません。より安全な方法については、envファイルとCompose secrets を参照してください。
次に、compose.override.yaml です。Composeが自動的に読み込む、開発者用のファイルです。
services:
app:
build: .
command: npm run dev
environment:
LOG_LEVEL: debug
ports:
- "3000:3000"
volumes:
- ./src:/app/src
db:
ports:
- "127.0.0.1:5432:5432"ラップトップでは、単独の docker compose up によってこの2つのファイルがマージされます。command は単一値のため、イメージのデフォルト値を置き換えます。LOG_LEVEL は、environment がキー単位でマージするため、info を置き換えます。バインドマウントと2つの公開ポートは追加されます。データベースのポートはlocalhostにバインドされるため、共有ネットワーク上の他のラップトップにPostgreSQLを公開しません。
最後に、compose.prod.yaml です。このファイル名はComposeが検索する名前ではないため、誤って読み込まれることはありません。
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MVPSでは両方のファイルを指定します。この指定によって、overrideファイルは確実に除外されます。
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml psps では、両方のサービスが実行中として表示され、db には (healthy) が表示されます。-f を渡したため、compose.override.yaml は読み込まれません。そのため、開発用コマンド、ソースのバインドマウント、公開ポート3000が同じディレクトリにファイルが存在していても本番環境に到達することはありません。ポート8000はlocalhostでのみ待ち受けるため、プロキシで使用できます。2つ目のサービスを追加するときは、Traefikの背後で複数のアプリケーションを実行する を参照してください。
サーバーの .env に COMPOSE_FILE=compose.yaml:compose.prod.yaml を設定すれば、以降のコマンドは通常の docker compose logs -f app に戻ります。
デプロイ前にマージ済みモデルを確認する
docker compose config は、完全にマージされ、すべての変数が展開されたモデルを出力します。これはプレビューではありません。Compose が処理する正確な入力です。そのため、出力が想定と異なる場合は、出力が正しい内容です。
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --services--no-interpolate は ${VAR} を展開しないままにします。出力をどこかに貼り付ける前に使用してください。通常の config は、解決済みのすべてのシークレットを平文で出力するためです。--services はサービス名のみを一覧表示します。これは、include が想定した内容を取り込んだことをすばやく確認する方法です。
失敗するケースと表示される内容
no configuration file provided: not found. Compose が読み込むファイルを見つけられませんでした。プロジェクトディレクトリの外にいるか、COMPOSE_FILE が存在しないパスを指定しています。Compose は親ディレクトリを検索してデフォルトのベースファイルを探しますが、自分で指定したファイルを探すことはありません。
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. 補間はプロジェクトの .env ファイルとシェル環境を基に解決されます。ここでのプロジェクトディレクトリは、最初の -f ファイルがあるディレクトリです。.env があるディレクトリとは別のディレクトリからデプロイすると、この警告が表示され、その後、どの接続も拒否するデータベースになります。
docker compose config に override の編集内容が反映されません。 -f を渡して自動的な override の読み込みを無効にしたか、Compose が親ディレクトリで compose.yaml を見つけたため、override ファイルがそのファイルと同じディレクトリにありません。ほかの引数を指定せずに docker compose config を実行すると、Compose が実際に構築しているモデルを確認できます。
bind mount が空で、Docker が指定していないディレクトリを作成します。 相対パスは最初のファイルがあるディレクトリを基準に解決されます。パスを修正するか、--project-directory を渡すか、フラグメントを include の後ろに移動します。
コンテナが新しい名前で再作成され、volume が空に見えます。 プロジェクト名が変わっています。プロジェクト名は最初のファイルがあるディレクトリに基づくためです。ベースファイルのトップレベルに name: を追加すると、名前が変わらなくなります。以前の volume は古いプレフィックスの下に残っており、docker volume ls で確認できます。
override で削除したポートがまだ開いています。 ports のマージで、置換ではなく追加が行われました。docker compose config で確認し、!override を使用するか、ports をベースファイルから移動します。
FAQ
Compose は compose.override.yaml を自動的に読み込みますか?
はい。-f フラグを指定せずに docker compose を実行すると読み込みます。Compose は作業ディレクトリとその親ディレクトリで compose.yaml または docker-compose.yaml を検索します。同じ場所に override ファイルがある場合、そのファイルを2番目に読み込みます。認識される名前は compose.override.yaml、compose.override.yml、docker-compose.override.yml、docker-compose.override.yaml です。-f を1つでも渡すとこの動作は無効になり、docker compose -f compose.yaml up は1つのファイルだけを読み込みます。
複数の -f ファイルはどの順序でマージされますか?
左から右の順です。Compose は指定された順序で設定を構築します。各ファイルは、それより前のファイルの設定を上書きまたは追加します。そのため、同じ設定が競合する場合は行の最後のファイルが優先されます。このプロジェクトのすべてのコマンドで同じリストを使用する必要があります。COMPOSE_FILE=compose.yaml:compose.prod.yaml はこのために使用します。
上書きした後もポートが公開されるのはなぜですか?
ports のエントリは、ip、target、published、protocol の組み合わせ全体で識別されるためです。8080:80 をベースに 127.0.0.1:8080:80 を上書きすると、ip の部分が異なります。そのため Compose はこれを2番目のポートとして扱い、両方を保持します。docker compose config を実行すると、2つのエントリを確認できます。Compose v2.24.4 以降では ports: !override を使用してください。または、マージ対象がなくなるようにベースファイルから ports を除外してください。
include と -f の違いは何ですか?
-f は複数のファイルを1つのアプリケーションに重ねます。すべてのファイルの相対パスは、最初のファイルのディレクトリを基準に解決されます。include は別の Compose アプリケーションを取り込みます。各 included path には独自のプロジェクトディレクトリが保持されるため、相対パスはそのアプリケーションを基準に解決されます。自分の stack の環境レイヤーには -f を使用し、別の場所で管理されている fragment には include を使用してください。include には Compose v2.20.0 以降が必要です。
ベースファイルで設定された値を削除するにはどうすればよいですか?
Compose v2.24 以降では !reset タグを使用してください。override ファイルに ports: !reset [] または MY_VAR: !reset null と記述すると、その属性はデフォルト値または null に戻ります。タグに指定する値は必須ですが、無視されます。属性を消去するのではなく置き換える場合は、!override を使用します。これには v2.24.4 以降が必要です。