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.yaml 或 docker-compose.yaml。如果覆寫檔案與基礎檔案位於同一目錄,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,就會停止自動載入。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 -dLinux 使用 : 作為分隔符號,而 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 會依值的類型合併,而不是依欄位名稱合併。
- 單值欄位會被取代。
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"]。
有四種序列帶有識別鍵,因此符合該鍵的項目會合併,而不是附加。volumes、secrets 和 configs 會依 target 比對。ports 則會依 ip、target、published 和 protocol 的組合比對。
請仔細閱讀兩次 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.0 與 127.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.yamlinclude 中的每個路徑都會以獨立的 Compose 應用程式模型載入,並使用各自的專案目錄。因此,該檔案中的相對路徑會以該檔案所在的目錄為基準解析。這正是它與 -f 的實際差異,也是 include 適合用於片段位於其他資料夾或其他 repository 的原因。供應商提供且非由您撰寫的 stack 通常就是這種形式:自架 Authentik SSO 安裝所使用的多服務 Compose 檔案,可以放在自己的目錄中並保留其相對路徑,而您的檔案則只需管理自己的服務。
長格式可接受子選項。
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath 接受清單,這些檔案會先依一般規則合併,再將結果加入您的模型。project_directory 設定用於解析 included 檔案中相對路徑的基準路徑。env_file 為 included 檔案提供專用的變數進行插值,避免共用片段無意間讀取您專案的 .env。include 需要 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 psps 應列出 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.yaml 或 docker-compose.yaml。如果 override 檔案與其位於同一目錄,Compose 會在第二階段載入該檔案。可識別的名稱包括 compose.override.yaml、compose.override.yml、docker-compose.override.yml 和 docker-compose.override.yaml。傳入任何 -f 都會停用此行為,因此 docker compose -f compose.yaml up 只會讀取一個檔案。
多個 -f 檔案會依什麼順序合併?
由左至右。Compose 會依照您提供檔案的順序建立設定。每個檔案都會覆寫並增加前面的檔案內容,因此命令列最後一個檔案會在衝突時優先。該專案的每個命令都必須使用相同的檔案清單,這就是 COMPOSE_FILE=compose.yaml:compose.prod.yaml 的用途。
為什麼我覆寫連接埠後,它仍然會發布?
因為 ports 項目是依 ip、target、published 和 protocol 的完整組合識別。以 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 或更新版本。