SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-30

Docker Compose 多檔案合併與 override 用法

了解 compose.override.yaml 何時會自動載入、-f 檔案順序如何合併、ports 清單為何可能保留埠號,以及使用 include 區分 dev 與 prod。

多個 Compose 檔案時,Docker Compose 的處理方式

Docker Compose 可以使用多個檔案建置單一專案。它會依收到檔案的順序讀取,並將檔案合併為單一模型。因此,後續檔案中的衝突值會覆寫先前的值。從命令列可透過兩種機制完成這項操作:Compose 自動載入的 override 檔案,以及手動傳入的 -f 旗標。第三種機制位於檔案本身,也就是 include 元素;它的運作方式與前兩者不同。

合併並不是單純覆寫。對應會逐一合併鍵值,序列會附加內容,少數欄位則會整個取代。差異會造成一些容易忽略的結果,而 ports 清單最容易讓人誤解。

以下內容都以 Compose v2 為前提,使用 docker compose 外掛程式,而不是舊版的 docker-compose 指令碼。執行 docker compose version 進行確認。如果你尚未撰寫 Compose 檔案,請先閱讀 Docker Compose 基礎指南,再回到這裡。

Compose 會自動載入的覆寫檔案

不使用 -f 旗標執行 docker compose up 時,Compose 會在工作目錄及其父目錄中搜尋 compose.yamldocker-compose.yaml。如果覆寫檔案與基礎檔案位於同一目錄,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.yamlcompose.override.yml,以及較舊的 docker-compose.override.ymldocker-compose.override.yaml。其他名稱,例如 compose.dev.yaml,只有在使用 -f 指定時才會載入。

一旦傳入 -f,就會停止自動載入。docker compose -f compose.yaml up 只會讀取該檔案,並忽略覆寫檔案;本指南稍後的 dev 和 prod 模式就是以此特性為基礎。

在伺服器上,這種行為可能造成兩方面的影響。部署目錄中留下的覆寫檔案,會被從該目錄執行的所有不帶參數的 docker compose 命令載入,包括 cron 工作執行的命令。這可能導致 production stack 綁定掛載原本不應部署的原始碼目錄。在每次部署後執行 docker compose config,並檢查輸出內容。部署由自動化程序執行時,只有在有人通知你檢查失敗,這項檢查才有幫助。這正是 push channel 的用途,例如 自架的 ntfy server;cron 工作或 systemd OnFailure unit 都能將通知送到該頻道。

使用 -f 的順序,以及相對路徑的解析位置

Compose 會依照您提供檔案的順序建立設定。後續檔案會覆寫並新增前置檔案的設定。由左至右處理,最後一個檔案優先。

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d

該專案中的每個命令都需要使用相同的檔案清單。使用兩個檔案執行 up,再使用一個檔案執行 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 檔案中,讓設定隨版本庫保存,而不是留在 shell 歷程記錄中。在命令列明確設定的值會優先於環境變數。

接下來是會造成 bind mount 問題的規則。使用 -f 載入多個檔案時,所有檔案中的相對路徑都會以第一個檔案所在的目錄為基準解析,而不是以包含該路徑的檔案為基準。在 deploy/prod/compose.prod.yaml 中寫入 ./data:/var/lib/postgresql/data 時,Compose 仍會在基礎檔案旁尋找 ./data。Docker 接著會在錯誤的路徑建立空目錄,容器啟動後其中沒有任何資料,看起來像是資料遺失,但實際上並非如此。請傳入 --project-directory 自行設定基準路徑,或使用 include,讓每個檔案都以自身所在的目錄為基準解析。

專案名稱也會取自相同的基準目錄,因此變更第一個檔案可能會重新命名專案。專案重新命名後,容器名稱與 volume 名稱都會變更,而舊 volume 仍會以舊名稱保留在磁碟上。請在基礎檔案的頂層設定 name:,固定專案名稱。

name: myapp

哪些欄位會合併,哪些欄位會取代

Compose 會依值的類型合併,而不是依欄位名稱合併。

  • 單值欄位會被取代。 imagecommandentrypointmem_limit 會直接採用後面的值。您無法在 command 中附加另一個引數,因為覆寫設定會改寫整行。
  • 映射會依鍵逐一合併。 environmentlabelsvolumesdevices 會保留兩個檔案中的所有鍵;若兩個檔案有相同的鍵,則採用後一個檔案的值。對 environmentlabels 而言,鍵是變數或標籤名稱。對 volumesdevices 而言,鍵是容器路徑。
  • 序列會附加。 dnsdns_searchexposetmpfsexternal_links 會串接。若基礎設定包含 expose: ["3000"],覆寫設定包含 ["4000", "5000"],合併後會產生 ["3000", "4000", "5000"]

有四種序列帶有識別鍵,因此符合該鍵的項目會合併,而不是附加。volumessecretsconfigs 會依 target 比對。ports 則會依 iptargetpublishedprotocol 的組合比對。

請仔細閱讀兩次 ports 規則,因為這是最容易出錯的地方。只有在四個部分全部相同時,兩個連接埠項目才會被視為同一個項目。只要其中任何一個部分不同,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.0127.0.0.1。因此,合併時它們會被視為兩個不同的連接埠,而你嘗試移除的公開綁定仍存在於模型中。這在 Docker 中尤其重要,因為發布的連接埠會寫入 iptables,而且優先於防火牆規則。相關機制請參閱為什麼已發布的 Docker 連接埠會繞過 ufw

有兩種修正方式。明確的方式是使用 !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 或更新版本。當基礎檔案不是由你維護時,請使用此功能,例如由供應商提供並載入的片段。已發布的上游堆疊正是這種情況:自架 AFFiNE 工作區背後的 Compose 檔案宣告了 4 個並非由你撰寫的容器,而 !reset 可讓你清除其中一個容器的單一屬性,不必建立檔案分支,也不必負責持續追蹤上游檔案的變更。

include:用於由多個部分組成的 stack

include 會將另一個 Compose 應用程式納入您的模型。這是頂層元素,不是旗標。

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

include 中的每個路徑都會以獨立的 Compose 應用程式模型載入,並使用各自的專案目錄。因此,該檔案中的相對路徑會以該檔案所在的目錄為基準解析。這正是它與 -f 的實際差異,也是 include 適合用於片段位於其他資料夾或其他 repository 的原因。供應商提供且非由您撰寫的 stack 通常就是這種形式:自架 Authentik SSO 安裝所使用的多服務 Compose 檔案,可以放在自己的目錄中並保留其相對路徑,而您的檔案則只需管理自己的服務。

長格式可接受子選項。

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

path 接受清單,這些檔案會先依一般規則合併,再將結果加入您的模型。project_directory 設定用於解析 included 檔案中相對路徑的基準路徑。env_file 為 included 檔案提供專用的變數進行插值,避免共用片段無意間讀取您專案的 .envinclude 需要 Compose v2.20.0 或更新版本。相同的選項也適用於加入現有 stack 的單容器附加元件,例如 Halcyon:將 Jellyfin 媒體庫重新設計為 90 年代租片店:其檔案會保留自己的 image tag 與 env_file,因此升級它時不必修改媒體 stack 所使用的檔案。

您的檔案與 included 檔案若有重複的資源名稱,Compose 會回報錯誤,而不是靜默合併。這是刻意的設計。若要變更 included 檔案宣告的內容,請將變更放入 compose.override.yaml:該覆寫會套用至組合後的模型,因此可以修改 included 資源,而不會與其發生衝突。對於上游檔案每次 release 都會改寫的 stack,這種做法特別實用,例如 PhotoPrism 與 Immich 的比較中評估的多容器相片伺服器;localhost 綁定或額外 volume 應放在您的覆寫檔中,而不是放入下次升級時會被取代的檔案。

簡而言之:include 用於組合不同的應用程式,-f 用於將設定分層套用至單一應用程式。

單一 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 會取代映像的預設值,因為它是單一值。LOG_LEVEL 會取代 info,因為 environment 會依鍵值合併。繫結掛載與 2 個發布連接埠都是額外加入的設定;資料庫連接埠則繫結至 localhost,因此在共用網路上的其他筆電無法存取 PostgreSQL。

最後是 compose.prod.yaml。Compose 不會尋找這個名稱,因此不會意外載入它。

services:
  app:
    ports:
      - "127.0.0.1:8000:3000"
    deploy:
      resources:
        limits:
          memory: 512M

在 VPS 上指定這 2 個檔案,而指定檔案的動作正好會排除 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 應列出 2 個服務正在執行,並由 db 顯示 (healthy)。由於你傳入了 -f,因此不會讀取 compose.override.yaml;即使該檔案位於同一個目錄中,開發環境的命令、來源繫結掛載與公開連接埠 3000 也無法影響正式環境。連接埠 8000 僅繫結至 localhost,可供代理使用;新增第二個服務時,請參閱 透過 Traefik 執行多個應用程式

在伺服器的 .env 中設定 COMPOSE_FILE=compose.yaml:compose.prod.yaml,其餘命令就能恢復使用單純的 docker compose logs -f app

單一服務的堆疊也採用相同結構,因為 自架 openGym 運動追蹤器 必須先在代理後方透過 TLS 回應,才能註冊第一個 passkey;而基礎檔案不包含 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 檔案與 shell 環境解析;此處的專案目錄是第一個 -f 檔案所在的目錄。從不同於 .env 所在目錄的位置進行部署時,會看到這項警告,接著資料庫會拒絕所有連線。

你的覆寫檔案修改未反映在 docker compose config 中。 你可能傳入了 -f,因此停用了自動載入覆寫檔案;或是 Compose 在父目錄找到 compose.yaml,而你的覆寫檔案不在同一個目錄中。不加其他引數執行 docker compose config,即可查看 Compose 實際建立的模型。

繫結掛載內容為空,且 Docker 建立了你未要求的目錄。 相對路徑是以第一個檔案所在的目錄為基準解析。修正路徑、傳入 --project-directory,或將該片段移至 include 之後。

容器重新啟動後名稱改變,且某個 volume 看起來是空的。 專案名稱已變更,因為專案名稱取決於第一個檔案所在的目錄。在基礎檔案中加入頂層 name:,即可固定名稱。舊的 volume 仍保留在舊的前綴下,使用 docker volume ls 即可查看。

你在覆寫檔案中移除的埠仍處於開放狀態。 ports 合併時採用附加而非取代。使用 docker compose config 確認,然後改用 !override,或將 ports 移出基礎檔案。

FAQ

Compose 會自動載入 compose.override.yaml 嗎?

會。執行 docker compose 且未使用 -f 旗標時,Compose 會在工作目錄及其父目錄中搜尋 compose.yamldocker-compose.yaml。如果 override 檔案與其位於同一目錄,Compose 會在第二階段載入該檔案。可識別的名稱包括 compose.override.yamlcompose.override.ymldocker-compose.override.ymldocker-compose.override.yaml。傳入任何 -f 都會停用此行為,因此 docker compose -f compose.yaml up 只會讀取一個檔案。

多個 -f 檔案會依什麼順序合併?

由左至右。Compose 會依照您提供檔案的順序建立設定。每個檔案都會覆寫並增加前面的檔案內容,因此命令列最後一個檔案會在衝突時優先。該專案的每個命令都必須使用相同的檔案清單,這就是 COMPOSE_FILE=compose.yaml:compose.prod.yaml 的用途。

為什麼我覆寫連接埠後,它仍然會發布?

因為 ports 項目是依 iptargetpublishedprotocol 的完整組合識別。以 8080:80 為基礎覆寫 127.0.0.1:8080:80 時,ip 部分不同,因此 Compose 會將其視為第二個連接埠,並保留兩者。執行 docker compose config 即可看到這兩個項目。在 Compose v2.24.4 或更新版本中使用 ports: !override;或者不要在基礎檔案中設定 ports,如此便沒有可供合併的項目。

include 和 -f 有什麼差異?

-f 會將多個檔案疊加至同一個應用程式,而每個檔案中的相對路徑都會以第一個檔案的目錄為解析基準。include 會納入另一個獨立的 Compose 應用程式,而每個被納入檔案的專案目錄仍各自保留,因此其相對路徑會以自身為解析基準。對於自有堆疊的環境層,請使用 -f;對於由其他位置維護的片段,請使用 includeinclude 需要 Compose v2.20.0 或更新版本。

如何移除基礎檔案設定的值?

在 Compose v2.24 或更新版本中,使用 !reset 標籤。在覆寫檔案中寫入 ports: !reset []MY_VAR: !reset null,該屬性就會恢復為預設值或 null。提供給該標籤的值雖為必要,但會被忽略。若要取代屬性而非清除屬性,請使用 !override;此功能需要 v2.24.4 或更新版本。