SSD Nodes Learn 8GB 記憶體 — 每年 $66
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-01

Docker Compose 多個檔案合併與 dev、prod 設定

了解 compose.override.yaml 如何自動載入、-f 檔案順序如何合併,以及 ports 清單為何仍會保留連接埠,並用 include 分離 dev 與 prod。

多個檔案時 Compose 的處理方式

Docker Compose 可以從多個檔案建置單一專案。它會依收到檔案的順序讀取,並將檔案合併成單一模型,因此後續檔案的衝突值會覆寫先前的值。命令列提供兩種機制:Compose 自動載入的覆寫檔案,以及您手動傳入的 -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 工作執行的命令。因此,正式環境的堆疊可能會繫結掛載原本無意部署的原始碼目錄。每次部署後執行 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

該專案中的每個命令都需要使用相同的檔案清單。使用兩個檔案執行 up,再使用一個檔案執行 logs,操作的就是不同的合併模型。這很容易導致 Compose 顯示服務不存在。請改用 COMPOSE_FILE 環境變數一次設定檔案清單。

export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -d

Linux 使用 : 作為分隔符號,而 COMPOSE_PATH_SEPARATOR 會變更此設定。您也可以將 COMPOSE_FILE 放在專案的 .env 檔案中,讓設定成為 checkout 的一部分,而不是 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 就會將其視為第二個無關的連接埠,因此會保留兩者。

為什麼套用覆寫設定後,連接埠仍然對外發布

在每個介面上發布服務的基礎檔案:

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"

將服務繫結至僅 localhost 的覆寫設定,因為前端會放置反向 Proxy:

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 或更新版本。當基礎檔案不是由您編輯時,例如您引入的廠商片段,請使用它。

include,適用於由多個部分組成的堆疊

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

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

include 中的每個路徑都會以獨立的 Compose 應用程式模型載入,並使用自己的專案目錄。因此,該檔案中的相對路徑會以該檔案所在的目錄為基準解析。這就是它與 -f 的實際差異,也是當片段位於其他資料夾或其他儲存庫時,應使用 include 的原因。

長格式可接受子選項。

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

path 接受清單,這些檔案會先依一般規則合併,再將結果加入您的模型。project_directory 設定用於解析所包含檔案中相對路徑的基底路徑。env_file 為所包含的檔案提供專屬變數,以進行插值,避免共用片段無意間讀取您專案的 .envinclude 需要 Compose v2.20.0 或更新版本。

如果您的檔案與所包含檔案之間有重複的資源名稱,系統會回報錯誤,而不會默默合併。這是刻意設計的行為。若要變更所包含檔案宣告的內容,請將變更放在 compose.override.yaml 中:覆寫會套用至組合後的模型,因此可以修改所包含的資源,而不會與其衝突。

簡而言之: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 條件會讓應用程式等待能夠回應的資料庫,而不是只等待容器存在。詳情請參閱 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 會取代 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 個檔案,而指定檔案的動作正好會排除覆寫檔案。

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,可供 proxy 使用;新增第 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 檔案和 shell 環境解析,而此處的專案目錄是第一個 -f 檔案所在的目錄。若從不同於 .env 所在目錄的位置進行部署,就會看到此警告,接著資料庫會拒絕所有連線。

您的覆寫編輯沒有出現在 docker compose config 中。 您可能傳入了 -f,因此停用了自動載入覆寫檔;或 Compose 在父目錄中找到 compose.yaml,而您的覆寫檔不在該檔案旁邊。不加其他引數執行 docker compose config,即可查看 Compose 實際建立的模型。

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

容器重新出現時名稱改變,且磁碟區看起來是空的。 專案名稱已變更,因為專案名稱取決於第一個檔案所在的目錄。請在基礎檔案中加入頂層 name:,即可停止名稱變動。舊磁碟區仍以舊前綴保留,執行 docker volume ls 即可查看。

您在覆寫檔中移除的連接埠仍然開放。 ports 合併時採用附加方式,而非取代方式。請使用 docker compose config 確認,然後使用 !override,或將 ports 移出基礎檔案。

FAQ

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

會。當您執行 docker compose 且未使用 -f 旗標時,Compose 會在工作目錄及其上層目錄中搜尋 compose.yamldocker-compose.yaml。如果覆寫檔案與其位於同一目錄,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 管理您自己堆疊的環境層,並使用 include 引入由其他位置維護的片段。include 需要 Compose v2.20.0 或更新版本。

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

請在 Compose v2.24 或更新版本使用 !reset 標籤。在覆寫檔案中寫入 ports: !reset []MY_VAR: !reset null,該屬性就會恢復為預設值或 null。您提供給該標籤的值是必要的,但會被忽略。如果您要取代屬性而不是清除屬性,!override 可以完成此操作,且需要 v2.24.4 或更新版本。