Docker Compose 複数ファイルのマージと override
Docker Compose の複数ファイルを読む順序とマージ規則を解説します。compose.override.yaml の自動読み込み、ports が残る理由、dev と prod を分ける include の使い方が分かります。
複数のファイルを指定したときの Compose の動作
Docker Compose は、複数のファイルから 1 つのプロジェクトを構築できます。Compose は受け取った順序でファイルを読み込み、単一のモデルにマージします。そのため、値が競合する場合は後のファイルの内容が優先されます。コマンドラインでは、これを行う仕組みが 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 は指定されたその 1 ファイルだけを読み込み、override ファイルは無視します。この動作を前提に、後述の dev と prod のパターンを構成しています。
サーバーでは、この点が逆方向にも作用します。デプロイディレクトリに残った override ファイルは、そのディレクトリから実行されるすべての bare docker compose コマンドで読み込まれます。cron ジョブが実行するコマンドも対象です。その結果、本番スタックが、誰も配布するつもりのなかったソースディレクトリを bind mount することがあります。デプロイ後は必ず docker compose config を実行し、出力内容を確認してください。デプロイを無人で実行する場合、問題の発生を通知する仕組みがなければ、この確認だけでは不十分です。cron ジョブまたは systemd OnFailure unit から投稿できる 自己ホスト ntfy サーバー のような push チャネルが、その役割を担います。
-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 が存在しないと報告するサービスを起動しようとすることになります。データベースのマイグレーションなど、アップグレードをワンショットコマンドで実行するスタックでは、影響がさらに大きくなります。たとえば セルフホストの Chatwoot サポートデスクで、誤ったファイル一覧を指定した docker compose run を実行すると、サービスがすでに使用しているモデルとは異なるモデルを、気付かないまま対象にします。代わりに COMPOSE_FILE 環境変数で一覧を一度だけ設定してください。
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -d区切り文字は Linux では : で、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に引数を追加することはできません。override によって行全体が書き換えられるためです。 - マッピングはキー単位でマージされます。
environment、labels、volumes、devicesでは両方のファイルのすべてのキーが保持され、両方に存在するキーは後のファイルの値が優先されます。environmentとlabelsでは、キーは変数名またはラベル名です。volumesとdevicesでは、キーはコンテナパスです。 - シーケンスは追加されます。
dns、dns_search、expose、tmpfs、external_linksは連結されます。expose: ["3000"]を持つベースに["4000", "5000"]を持つ override をマージすると、["3000", "4000", "5000"]になります。
4 つのシーケンスには識別キーがあります。そのため、このキーが一致するエントリは追加されずにマージされます。volumes、secrets、configs は target で一致判定されます。ports は ip、target、published、protocol の組み合わせで一致判定されます。
この ports のルールは、特に注意して確認してください。4 つの要素がすべて一致する場合に限り、2 つのポートエントリは同じエントリとみなされます。いずれか 1 つを変更すると、Compose は別の無関係なポートとして扱うため、両方が保持されます。
オーバーライド後もポートが公開されたままになる理由
すべてのインターフェイスでサービスを公開するベースファイルです。
services:
web:
image: nginx:1.27
ports:
- "8080:80"リバースプロキシを前段に置くため、localhost のみにバインドするよう記述したオーバーライドです。
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 以降が必要です。ベースファイルを編集できない場合に使用します。たとえば、ベンダーのフラグメントを取り込む場合です。公開された upstream のスタックは、まさにこのケースに該当します。self-hosted AFFiNE ワークスペースの背後にある Compose ファイルでは、自分で作成していない4つのコンテナが定義されています。!reset を使えば、ファイルを fork してその追跡管理まで引き受けることなく、そのうち1つの属性をクリアできます。
構成要素から組み立てたスタックには include を使う
includeは別の Compose アプリケーションをモデルに取り込みます。これはトップレベル要素であり、フラグではありません。
include:
- path: ../commons/compose.yamlincludeに指定した各パスは、それぞれ独立した Compose アプリケーションモデルとして読み込まれます。各モデルには固有のプロジェクトディレクトリがあるため、そのファイル内の相対パスはファイル自身のディレクトリを基準に解決されます。これが-fとの実質的な違いであり、フラグメントが別のフォルダーや別のリポジトリにある場合にincludeが適している理由です。これは、自分で作成していないベンダー提供のスタックでよくある構成です。自ホストのAuthentik SSOの構成にある複数サービス用のComposeファイルを、相対パスを維持したまま専用ディレクトリに置けます。その場合も、自分のファイルは自分のサービスに集中できます。
長形式ではサブオプションを指定できます。
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpathにはリストを指定できます。指定したファイルは通常のルールでマージされ、その結果がモデルに追加されます。project_directoryは、インクルードしたファイル内の相対パスを解決する基準パスを指定します。env_fileを使うと、インクルードしたファイルに独自の変数を指定できます。これにより、共有フラグメントがプロジェクトの.envを意図せず読み込むことを防げます。includeにはCompose v2.20.0以降が必要です。これらのオプションは、既存のスタックに単一コンテナーのアドオンを追加する場合にも適しています。たとえば、Jellyfinのライブラリを90年代のレンタル店風に変更するHalcyonでは、ファイルが独自のイメージタグと独自のenv_fileを保持します。そのため、アップグレード時にメディアスタックのファイルを変更する必要がありません。
自分のファイルとインクルードしたファイルでリソース名が重複すると、静かにマージされず、エラーとして報告されます。これは意図された動作です。インクルードしたファイルの宣言を変更する場合は、compose.override.yamlに変更を記述します。オーバーライドは組み立て後のモデルに適用されるため、インクルードしたリソースにも、名前の衝突を起こさずに変更を加えられます。この方法は、アップストリームのファイルがリリースごとに書き換えられるスタックで特に有効です。たとえば、PhotoPrismとImmichの比較で検討されている複数コンテナーのフォトサーバーでは、localhostへのバインドや追加ボリュームを、次回のアップグレードで置き換えられるファイルではなく、オーバーライドに記述します。
要点は次のとおりです。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 条件により、コンテナが存在するだけでなく、応答可能なデータベースを待機してからアプリを起動できます。詳しくは healthcheck と 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 はイメージのデフォルト値を置き換えます。environment はキー単位でマージするため、LOG_LEVEL は 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 に戻せます。
単一サービスのスタックでも同じ構成が適しています。self-hosted openGym workout tracker は、最初の passkey を登録する前に、プロキシの背後で TLS 経由の応答を提供する必要があります。ports を含まないベースファイルを使えば、意図しない公開バインドがプロキシより先にポートを使用することを防げます。
デプロイ前にマージ済みモデルを確認する
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 は、解決済みのすべての Secret を平文で出力するためです。--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 の後ろへ移動してください。
コンテナが新しい名前で起動し、ボリュームが空に見えます。 プロジェクト名が変わっています。プロジェクト名は最初のファイルのディレクトリに基づくためです。ベースファイルにトップレベルの name: を追加すると、名前が変わらなくなります。古いボリュームは以前のプレフィックス付きで残っており、docker volume ls で確認できます。
override で削除したポートがまだ開いています。 ports のマージで、置換ではなく追加が行われています。docker compose config で確認し、!override を使用するか、ports をベースファイルの外へ移動してください。
FAQ
Compose は compose.override.yaml を自動的に読み込みますか?
はい。-f flag なしで docker compose を実行すると、自動的に読み込みます。Compose は作業ディレクトリとその親ディレクトリで compose.yaml または docker-compose.yaml を検索します。同じディレクトリに override file がある場合は、2 番目に読み込まれます。認識される名前は compose.override.yaml、compose.override.yml、docker-compose.override.yml、docker-compose.override.yaml です。-f を 1 つでも指定するとこの動作は無効になり、docker compose -f compose.yaml up は 1 つの file だけを読み込みます。
複数の -f file はどの順序でマージされますか?
左から右です。Compose は指定された順序で configuration を構築します。各 file は、それより前の file の設定を上書きし、設定を追加します。そのため、競合した場合は行の最後にある file が優先されます。この project のすべての command で同じ list を使用する必要があります。これを指定するのが COMPOSE_FILE=compose.yaml:compose.prod.yaml です。
上書きした後も port が公開されるのはなぜですか?
ports の entry は、ip、target、published、protocol の組み合わせ全体で識別されるためです。8080:80 を base にして 127.0.0.1:8080:80 を上書きすると、ip の部分が異なります。そのため Compose は 2 番目の port として扱い、両方を保持します。docker compose config を実行すると、2 つの entry を確認できます。Compose v2.24.4 以降では ports: !override を使用してください。または、マージ対象がなくなるように base file から ports を削除してください。
include と -f の違いは何ですか?
-f は複数の file を 1 つの application に重ねます。すべての file の相対 path は、最初の file のディレクトリを基準に解決されます。include は別の Compose application を取り込みます。各 included path は独自の project directory を維持するため、相対 path はその application 自身を基準に解決されます。自分の stack の environment layer には -f を使用し、別の場所で管理されている fragment には include を使用します。include には Compose v2.20.0 以降が必要です。
base file で設定された値を削除するにはどうすればよいですか?
Compose v2.24 以降では、!reset tag を使用します。override file に ports: !reset [] または MY_VAR: !reset null を記述すると、その attribute は default 値または null に戻ります。tag に指定する値は必須ですが、内容は無視されます。attribute をクリアするのではなく置き換える場合は !override を使用します。これには v2.24.4 以降が必要です。