SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-10-30

Docker Compose 複数ファイルのマージと override

Docker Compose の複数ファイルを読む順序とマージ規則を解説します。compose.override.yaml の自動読み込み、ports が残る理由、dev と prod を分ける include の使い方が分かります。

複数のファイルを指定した場合に Compose が行う処理

Docker Compose では、複数のファイルから 1 つのプロジェクトを構成できます。指定された順番にファイルを読み込み、1 つのモデルにマージするため、値が競合する場合は後のファイルが優先されます。コマンドラインからこれを行う仕組みは 2 つあります。Compose が自動的に読み込む override ファイルと、手動で指定する -f フラグです。3 つ目の仕組みはファイル内にある include 要素で、前の 2 つとは動作が異なります。

マージは単純な上書きではありません。マッピングはキー単位でマージされ、シーケンスは追加されます。一部のフィールドは全体が置き換えられます。この違いが予期しない結果の原因になります。特に、ports リストではほとんどの利用者が問題に遭遇します。

以下ではすべて Compose v2 以降を前提とします。古い docker-compose スクリプトではなく、docker compose プラグインを使用します。現在のリリースは v5 で、プラグインの動作も同じです。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 -d

Compose が認識する名前は 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.18.0 以降で使用できます。!overrideが追加される数回のリリース前から利用可能です。ベースファイルを編集できない場合に使用します。たとえば、ベンダーが提供するフラグメントを取り込む場合です。公開されている upstream スタックは、まさにこのケースに該当します。自己ホスト型 AFFiNE ワークスペースで使用する Compose ファイルには、自分で記述していない4つのコンテナが定義されています。!resetを使えば、ファイルを fork してその追跡作業を引き継ぐことなく、そのうち1つの属性を消去できます。

include、部品から組み立てるスタック向け

include は、別の Compose アプリケーションをモデルに取り込みます。これはトップレベル要素であり、フラグではありません。

include:
  - path: ../commons/compose.yaml

include の各パスは、それぞれ独自の Compose アプリケーションモデルとして読み込まれます。各モデルには独自のプロジェクトディレクトリがあるため、そのファイル内の相対パスは、そのファイル自身のディレクトリを基準に解決されます。これが -f との実際の違いです。フラグメントが別のフォルダーや別のリポジトリにある場合に include が適している理由でもあります。これは、自分で作成していないベンダー提供のスタックで一般的な構成です。たとえば、自己ホスト型 Authentik SSO のインストールで使用する複数サービスの Compose ファイルを専用ディレクトリに置き、独自の相対パスをそのまま維持できます。その場合でも、ローカルのファイルは自分のサービスに集中できます。

長い形式では、サブオプションを指定できます。

include:
  - path:
      - ../monitoring/compose.yaml
      - ../monitoring/compose.vps.yaml
    project_directory: ../monitoring
    env_file: ../monitoring/.env

path にはリストを指定できます。指定したファイルは通常のルールでマージされてから、結果がモデルに追加されます。project_directory は、インクルードしたファイル内の相対パスを解決する際の基準パスを設定します。env_file は、インクルードした Compose ファイルが補間のデフォルト値を取得するファイルを指定します。デフォルトでは、そのファイル自身のプロジェクトディレクトリにある .env が使用されます。ただし、ローカルプロジェクトの環境がそれより優先されます。そのため、フラグメントを編集せずに、その変数の 1 つを上書きできます。include には Compose v2.20.0 以降が必要です。同じオプションは、既存のスタックに単一コンテナーのアドオンを追加する場合にも適しています。たとえば、Jellyfin のライブラリを 90 年代のレンタル店風にする Halcyon です。そのファイルは独自のイメージタグと独自の env_file を保持するため、アップグレードのたびにメディアスタックのファイルを変更する必要がありません。

Docker のドキュメントでは、ファイルとインクルードしたファイルの両方で宣言されたリソース名は、競合として拒否されると説明されています。しかし、現在のリリースのローダーはそのように動作しません。2026-10-27 に、同梱されているローダーのソースコードで確認した Compose v5.6.1 では、2 つの定義をマージします。値が競合する場合は自分のファイルが優先され、エラーで停止することはありません。したがって、重複した名前が明確なエラーになることを前提にしないでください。インクルードしたファイルで宣言されている内容を変更する場合は、変更を 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: 512M

VPS では両方のファイルを指定します。この指定によって、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 ps

ps には両方のサービスが起動中として表示され、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 を自動的に読み込みますか?

はい。docker compose を -f フラグなしで実行すると、自動的に読み込みます。Compose は作業ディレクトリとその親ディレクトリを検索し、compose.yaml または docker-compose.yaml を探します。同じディレクトリに override ファイルがあれば、2 番目に読み込まれます。認識される名前は compose.override.yaml、compose.override.yml、docker-compose.override.yml、docker-compose.override.yaml です。-f を渡すとこの動作が無効になり、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 アプリケーションを取り込みます。取り込んだ各ファイルは独自のプロジェクトディレクトリを保持するため、相対パスはそのファイル自身を基準に解決されます。自分のスタックの環境レイヤーには -f を使用し、別の場所で管理されているフラグメントには include を使用してください。include には Compose v2.20.0 以降が必要です。

ベースファイルで設定された値を削除するにはどうすればよいですか?

Compose v2.18.0 以降では、!reset タグを使用します。上書き用ファイルに ports: !reset [] または MY_VAR: !reset null を記述すると、その属性はデフォルト値または null に戻ります。タグに指定する値は必須ですが、無視されます。属性をクリアするのではなく置き換える場合は、!override を使用します。この機能には v2.24.4 以降が必要です。

この記事は役に立ちましたか?