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.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 工作執行的命令。因此,正式環境的堆疊可能會繫結掛載原本無意部署的原始碼目錄。每次部署後執行 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 -dLinux 使用 : 作為分隔符號,而 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 會根據值的類型合併,而不是根據欄位名稱合併。
- 單值欄位會被取代。
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 就會將其視為第二個無關的連接埠,因此會保留兩者。
為什麼套用覆寫設定後,連接埠仍然對外發布
在每個介面上發布服務的基礎檔案:
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.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 或更新版本。當基礎檔案不是由您編輯時,例如您引入的廠商片段,請使用它。
include,適用於由多個部分組成的堆疊
include 會將另一個 Compose 應用程式納入您的模型。它是頂層元素,不是旗標。
include:
- path: ../commons/compose.yamlinclude 中的每個路徑都會以獨立的 Compose 應用程式模型載入,並使用自己的專案目錄。因此,該檔案中的相對路徑會以該檔案所在的目錄為基準解析。這就是它與 -f 的實際差異,也是當片段位於其他資料夾或其他儲存庫時,應使用 include 的原因。
長格式可接受子選項。
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 或更新版本。
如果您的檔案與所包含檔案之間有重複的資源名稱,系統會回報錯誤,而不會默默合併。這是刻意設計的行為。若要變更所包含檔案宣告的內容,請將變更放在 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 psps 應列出 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.yaml 或 docker-compose.yaml。如果覆寫檔案與其位於同一目錄,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 或更新版本。