容器化技術 ch11 docker compose up 逐行解剖
下一章→
CH 11 Compose 主線

docker compose up 逐行解剖

專案概念與資源命名檔案每一段的意義depends_on 的真相環境變數的三層來源多檔覆蓋與 profiles

你最熟悉的就是這個指令——但也是最黑箱的一個。

ch01 說過 docker compose up 背後有六件事,前面十章已經把每件事的原理都講完了。這一章把 Compose 本身徹底拆開:它的專案概念、檔案每一段的意義、以及那個讓無數人卡住的 depends_on——它其實不會等服務準備好。

理解之後你就不只是「照著範例改」,而是能自己設計一整套開發環境了(那是下一章)。

Compose 沒有發明任何新技術。它做的事,你用 docker 指令全部都做得到——只是要打很多字,而且沒有紀錄。

對照一下就懂了

# 沒有 Compose 的世界:每次都要打這些,還要記得順序
docker network create myapp-net
docker volume create mssql-data
docker run -d --name sqlserver --network myapp-net \
  -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=xxx \
  -p 1433:1433 -v mssql-data:/var/opt/mssql \
  mcr.microsoft.com/mssql/server:2022-latest
docker run -d --name app --network myapp-net -p 8080:8080 \
  -e SPRING_DATASOURCE_URL=jdbc:sqlserver://sqlserver:1433 myapp:1.0
# 有 Compose 的世界:寫成檔案,一句指令
docker compose up -d
Compose 的三個價值:
① 宣告式——你描述「我要的最終狀態」,工具負責達成(ch01 那個思維)。
② 可進版控——整套環境的定義變成一份可審查、可回溯的檔案。
③ 自動處理關聯——網路、volume、啟動順序都不用你手動串。

它的定位(很重要,避免誤用)

  • Compose 是「單機」工具:所有容器跑在同一台機器上。
  • 它非常適合本機開發環境、CI 的整合測試、小型單機部署。
  • 但它不是正式環境的部署方案——沒有跨機器調度、沒有自動故障轉移、沒有滾動更新。那些是 Kubernetes 的工作(ch14)。
⭐ 版本演進的小提醒:舊的 docker-compose(有連字號,Python 寫的獨立工具)已被 docker compose(無連字號,Go 寫的 Docker CLI 外掛)取代。檔案開頭的 version: '3.8' 也已經過時,現在可以直接省略——新版會忽略它,寫了反而可能收到警告。

這個概念沒搞清楚,會遇到一堆詭異現象。

Compose 把一份 compose 檔啟動的所有資源,歸屬於一個「專案(project)」。專案名稱預設就是 compose 檔所在的目錄名稱。

所有資源都會被加上專案名前綴

目錄名:myapp/

容器: myapp-sqlserver-1        ← 專案名-服務名-序號
網路: myapp_default            ← 自動建立的專屬網路
Volume:myapp_mssql-data        ← 具名 volume 也會加前綴

由此解釋三個常見困惑

  1. 「我把資料夾改名,結果資料全不見了!」
    因為專案名變了 → volume 名稱從 myapp_mssql-data 變成 newname_mssql-data → 那是一個全新的空 volume。舊的資料還在,只是沒被掛上。
  2. 「同一份 compose 檔複製到兩個資料夾,變成兩套獨立環境」
    對,這是特性不是 bug。想同時跑兩套(例如兩個分支)就這樣做,或用 -p 指定不同專案名。
  3. 「A 專案的 app 連不到 B 專案的資料庫」
    因為各自的預設網路不同(ch07 說的網路隔離)。要跨專案連線得用 external 網路明確共用。
# 明確指定專案名(不依賴目錄名,CI 上特別有用)
docker compose -p myproject up -d

# 或寫在檔案裡
name: myproject
services:
  ...
💡 實用推論:CI 上同時跑多個測試任務時,用 -p ci-${BUILD_ID} 給每個任務獨立的專案名,它們就完全不會互相干擾(各自的網路、各自的 volume、各自的容器名)。
name: myapp                          # 專案名(可省略,預設用目錄名)

services:                            # ① 服務定義(主體)
  app:
    build:                           # 用 Dockerfile 建置(與 image 二選一)
      context: .                     #   build context(ch08 那個點)
      dockerfile: Dockerfile
      args:                          #   對應 Dockerfile 的 ARG
        JAR_FILE: target/app.jar
    image: myapp:1.0                 # 建置後標記的名字;單獨用則是直接拉取
    ports:
      - '8080:8080'                  # 宿主機:容器(ch07)
    environment:                     # 環境變數(ch09 設定注入)
      SPRING_PROFILES_ACTIVE: dev
      SPRING_DATASOURCE_URL: jdbc:sqlserver://sqlserver:1433;databaseName=mydb
    env_file:
      - .env                         # 從檔案批次載入環境變數
    volumes:
      - ./logs:/app/logs             # bind mount(ch06)
    depends_on:                      # 啟動順序(有大坑,見下一張卡)
      sqlserver:
        condition: service_healthy
    restart: unless-stopped          # 重啟策略(ch05)
    healthcheck:                     # 健康檢查(ch05)
      test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/actuator/health']
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s              # Java 冷啟動一定要留
    deploy:
      resources:
        limits:                      # cgroup 上限(ch02)
          memory: 1G
          cpus: '1.5'
    networks:
      - backend

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: 'Y'
      MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD}   # 從 .env 展開(見下下張卡)
    volumes:
      - mssql-data:/var/opt/mssql
    networks:
      - backend

volumes:                             # ② 具名 volume 宣告(ch06)
  mssql-data:

networks:                            # ③ 網路宣告(ch07)
  backend:

幾個容易搞混的細節

  • build vs image:兩者都寫時,用 build 建置,然後標記成 image 指定的名字。只寫 image 就是直接拉現成的。
  • environment vs env_file:前者寫在檔案裡(會進版控,不要放密碼),後者從外部檔案讀(.env 要 gitignore)。同名時 environment 優先。
  • ports vs expose:ports 是真的對外映射;expose 只是宣告(ch07)。只給其他容器用的服務不要寫 ports——資料庫不對外開,安全性直接提升一大截。
  • deploy.resources:在 Swarm 模式外,docker compose 仍會套用 limits。想更明確也可以用頂層的 mem_limit、cpus。

這是 Compose 最多人誤解、也最常導致「第一次啟動失敗、重啟就好」的地方。

services:
  app:
    depends_on:
      - sqlserver      # 直覺:等資料庫好了再啟動 app
錯。這種寫法只保證「sqlserver 容器被啟動」,不保證「SQL Server 可以接受連線」。

容器啟動只需要幾百毫秒(ch03:就是掛個可寫層再 execve),但 SQL Server 從行程啟動到能接受 TCP 連線要 20~60 秒。Compose 早在第一秒就啟動了 app,於是 app 衝過去連線 → Connection refused → 崩潰。

解法一:healthcheck + condition(Compose 層的正解)

services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    healthcheck:
      test: ['CMD-SHELL', '/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$$MSSQL_SA_PASSWORD" -C -Q "SELECT 1" || exit 1']
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 30s

  app:
    depends_on:
      sqlserver:
        condition: service_healthy    # ← 關鍵:等到 healthcheck 通過才啟動
  • $$ 是逃脫寫法:Compose 會展開 $VAR,寫 $$ 才能把 $ 原樣傳給容器內的 shell。這個細節卡過很多人。
  • 三種 condition:service_started(預設,只等啟動)、service_healthy(等健康檢查通過)、service_completed_successfully(等它跑完並成功結束,適合初始化任務)。

解法二:應用層自己重試(真正的正解)

🚨 為什麼 healthcheck 還不夠?因為正式環境沒有 Compose 幫你排順序。在 K8s 裡,Pod 的啟動順序不保證;而且服務在運行期間也可能短暫斷線(資料庫重啟、網路抖動、故障轉移)。一個健壯的應用本來就該能承受「相依服務暫時不可用」。
# Spring Boot:連線池自帶重試與健康檢查
spring:
  datasource:
    hikari:
      initialization-fail-timeout: -1   # 啟動時連不上也不要直接死,繼續重試
      connection-timeout: 30000

# 或用 Resilience4j 對外部呼叫加上重試與斷路器

把 Compose 的 healthcheck 當成「讓開發體驗順暢」的優化,把應用層重試當成「正確性的保證」。兩者並用最理想。

解法三:等待腳本(遺留做法,了解即可)

過去常見用 wait-for-it.sh、dockerize 之類的腳本,在容器啟動指令前先輪詢對方的 port。缺點是「port 開了」不等於「服務就緒」(資料庫可能還在跑復原程序),而且要在映像裡多裝東西。有 condition: service_healthy 之後已不建議。

「.env 到底是給誰用的?」——這是 Compose 最常被搞混的地方。關鍵在於分清楚「變數用在 compose 檔裡」還是「變數送進容器裡」。

第一層:.env 檔 → 給「compose 檔本身」做變數展開

# .env(放在 compose 檔旁邊,Compose 自動讀取)
MSSQL_SA_PASSWORD=YourStrong!Passw0rd
APP_PORT=8080
# docker-compose.yml
services:
  app:
    ports:
      - '${APP_PORT}:8080'          # ← 在「解析 compose 檔的當下」被替換掉
重點:.env 裡的變數不會自動送進容器!它只是讓你在 compose 檔裡可以寫 ${VAR}。很多人以為在 .env 寫了 SPRING_PROFILES_ACTIVE=dev,容器裡就有這個環境變數——沒有。

第二層:environment → 真正送進容器的環境變數

services:
  app:
    environment:
      SPRING_PROFILES_ACTIVE: dev                    # 寫死
      MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD}        # 從 .env 取值再送進去 ✅
      DB_PASSWORD:                                   # 只寫名字 = 從 shell 環境繼承

第三層:env_file → 從檔案批次載入到容器

services:
  app:
    env_file:
      - ./config/app.env      # 這個檔案裡的每一行都會變成容器的環境變數 ✅

優先序(高到低)

  1. docker compose run -e KEY=VAL(指令列)
  2. compose 檔的 environment
  3. env_file 指定的檔案
  4. Dockerfile 裡的 ENV

兩個實用語法

${VAR:-預設值}     # VAR 沒設定或為空時使用預設值
${VAR:?錯誤訊息}   # VAR 沒設定就直接報錯中止(強制必填,很好用)
$$VAR             # 逃脫:不要展開,原樣傳給容器內的 shell
⭐ 驗證合併結果的萬用招:
docker compose config
它會印出所有變數展開、所有覆蓋檔合併之後的最終設定。「我明明設定了怎麼沒生效」的第一個排查動作就是這個——不要用猜的。

自動合併:docker-compose.override.yml

Compose 預設會自動讀取並合併這兩個檔案:

docker-compose.yml            # 共通設定(進版控)
docker-compose.override.yml   # 本機開發專用的覆蓋(可以 gitignore)
# docker-compose.yml —— 基礎
services:
  app:
    image: myapp:1.0
    environment:
      SPRING_PROFILES_ACTIVE: prod

# docker-compose.override.yml —— 開發時自動疊上去
services:
  app:
    build: .                          # 開發時改成本機建置
    environment:
      SPRING_PROFILES_ACTIVE: dev     # 覆蓋掉 prod
    volumes:
      - ./src:/app/src                # 開發才需要的原始碼掛載
    ports:
      - '5005:5005'                   # 遠端除錯埠

明確指定多檔(CI/不同環境用)

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
#               ↑ 基礎              ↑ 後面的覆蓋前面的
🚨 合併規則有個陷阱:純量值(字串、數字)是「覆蓋」,但清單(如 ports、volumes)預設是「合併相加」——所以你在覆蓋檔寫 ports 不會取代原本的,而是兩個都存在。要真正取代得用 !reset 或重新設計結構。不確定結果時,一律先跑 docker compose config 看合併後長什麼樣。

Profiles:可選的服務

services:
  app:
    image: myapp:1.0
  adminer:                # 資料庫管理介面,不是每次都要開
    image: adminer
    profiles: ['tools']
  test-runner:
    image: myapp:test
    profiles: ['test']
docker compose up -d                      # 只啟動沒有 profile 的服務(app)
docker compose --profile tools up -d      # app + adminer
docker compose --profile test up          # app + test-runner

日常工作流指令

docker compose up -d --build          # 重新建置並啟動
docker compose up -d --no-deps app     # 只重啟 app,不動它的相依服務 ⭐常用
docker compose logs -f app             # 跟隨單一服務的日誌
docker compose exec app bash           # 進某個服務的容器
docker compose ps                      # 看狀態(含 healthy 與否)
docker compose restart app             # 重啟單一服務
docker compose down                    # 停止並移除容器與網路(volume 保留)
docker compose down -v                 # 💣 連 volume 一起刪(ch06)
docker compose config                  # 印出合併後的最終設定
depends_on 的三種等待策略
寫法實際等到什麼夠不夠適用
depends_on: [db]只等容器「被啟動」(幾百毫秒)不夠——DB 要 20~60 秒才能接受連線幾乎沒有適用情境
condition: service_started同上(明確寫法)不夠相依服務啟動極快時
condition: service_healthy等 healthcheck 通過開發環境足夠Compose 層的正解
condition: service_completed_successfully等它跑完且結束碼為 0適用於一次性任務資料庫遷移、初始化任務
應用層重試自己處理連不上的情況唯一在正式環境也成立的解所有情況都該做(K8s 沒有 compose 排順序)
環境變數三層來源
來源作用對象會進容器嗎典型用途
.env 檔compose 檔本身的 ${VAR} 展開不會!把密碼/埠號抽出版控之外
environment:容器的環境變數會設定注入(Spring 的 SPRING_* 等)
env_file:容器的環境變數(批次)會大量設定集中管理
Dockerfile ENV映像內建的預設值會(優先序最低)合理的預設,不放密碼
Compose 日常指令速查
指令作用備註
up -d --build重新建置並在背景啟動改了 Dockerfile 之後用
up -d --no-deps app只重啟 app,不動相依服務開發時最常用,不會重啟資料庫
config印出變數展開與多檔合併後的最終設定「設定沒生效」的第一個排查動作
logs -f app跟隨單一服務日誌加 --tail 100 只看最近
ps看狀態會顯示 healthy/unhealthy
down停止並移除容器與網路volume 保留 ✅
down -v連 volume 一併刪除💣 資料全毀,無確認提示

練習題 點選選項查看解析

0 / 9
01 / 9
你把專案資料夾改名後執行 docker compose up,發現資料庫的資料全空了。為什麼?
A Compose 自動清空了資料
B 專案名稱預設取自目錄名,所有資源都加上這個前綴——改名後 volume 從 myapp_mssql-data 變成 newname_mssql-data,那是一個全新的空 volume(舊資料還在,只是沒被掛上)
C 改名破壞了映像
D 需要重新執行 docker volume create
解析
理解「專案」概念可以解釋一連串現象:同一份 compose 檔複製到兩個資料夾會變成兩套獨立環境;不同專案的服務預設在不同網路所以連不到彼此。要避免依賴目錄名,用 docker compose -p myproject 或在檔案裡寫 name: myproject。
02 / 9
depends_on: [sqlserver] 實際保證了什麼?
A 保證 SQL Server 已經可以接受連線
B 只保證 sqlserver 容器「被啟動」(幾百毫秒),完全不保證裡面的服務已就緒——而 SQL Server 要 20~60 秒才能接受連線
C 保證兩個容器同時啟動
D 保證 sqlserver 的健康檢查通過
解析
這是 Compose 最多人誤解的地方,症狀就是「第一次啟動失敗、手動重啟 app 就好」。容器啟動只是掛個可寫層再 execve(ch03、ch05),跟裡面的服務要多久才能服務完全是兩回事。
03 / 9
要讓 app 真的等到資料庫可以接受連線才啟動,Compose 層的正解是?
A 在 app 的啟動指令前加 sleep 30
B 為 sqlserver 定義 healthcheck,並在 app 的 depends_on 用 condition: service_healthy
C 調整 depends_on 的服務順序
D 使用 restart: always 讓它一直重試
解析
sleep 是猜測不是保證(機器慢一點就失敗)。condition 有三種:service_started(預設,只等啟動)、service_healthy(等健康檢查通過)、service_completed_successfully(等一次性任務成功結束,適合資料庫遷移)。
04 / 9
既然有了 condition: service_healthy,為什麼還說「應用層重試」才是真正的正解?
A 因為 healthcheck 會消耗太多資源
B 因為正式環境沒有 Compose 幫你排順序(K8s 不保證 Pod 啟動順序),而且服務在運行期間也可能短暫斷線——健壯的應用本來就該能承受相依服務暫時不可用
C 因為 healthcheck 不準確
D 因為 Compose 未來會移除這個功能
解析
把 Compose 的 healthcheck 當成「讓開發體驗順暢」的優化,把應用層重試當成「正確性的保證」。Spring Boot 可設 hikari 的 initialization-fail-timeout: -1 讓啟動時連不上也繼續重試,或用 Resilience4j 加上重試與斷路器。
05 / 9
你在 .env 檔寫了 SPRING_PROFILES_ACTIVE=dev,但容器裡讀不到這個環境變數。為什麼?
A 檔名應該是 env.txt
B .env 是給「compose 檔本身」做 ${VAR} 變數展開用的,不會自動送進容器;要送進容器必須在 environment 或 env_file 明確指定
C 需要重新啟動 Docker 服務
D 變數名稱需要加上 DOCKER_ 前綴
解析
這是 Compose 最常見的混淆。正確寫法是在 environment 底下寫 SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE},或直接用 env_file: [.env] 把整個檔案載入成容器環境變數。搞不清楚時就跑 docker compose config 看最終結果。
06 / 9
healthcheck 的 test 裡寫 $$MSSQL_SA_PASSWORD(兩個錢字號)的用意是?
A 這是打錯字
B 逃脫寫法:Compose 會展開 $VAR,寫 $$ 才能把單一個 $ 原樣傳給容器內的 shell 去展開
C 表示這是必填變數
D 代表要展開兩次
解析
這個細節卡過很多人:寫一個 $ 的話 Compose 在解析檔案時就把它換掉了,容器裡的 shell 收到的是空字串或錯誤的值。相關語法還有 ${VAR:-預設值} 和 ${VAR:?錯誤訊息}(沒設定就直接報錯中止,用來強制必填很好用)。
07 / 9
「我明明改了設定卻沒生效」時,第一個該執行的指令是?
A docker compose restart
B docker compose config——印出所有變數展開、多檔合併後的最終設定,直接看到 Compose 實際認定的內容
C docker compose down && up
D docker system prune
解析
不要用猜的。config 會把 .env 展開、override 檔合併、預設值填入之後的完整結果印出來。尤其在使用多檔覆蓋時特別重要——因為合併規則有陷阱:純量值是覆蓋,但清單(ports、volumes)預設是相加不是取代。
08 / 9
開發時只想重新建置並重啟 app,不要動到正在跑的資料庫。該用哪個指令?
A docker compose restart
B docker compose up -d --build --no-deps app
C docker compose down && docker compose up -d
D docker compose exec app restart
解析
--no-deps 表示不要連帶處理它的相依服務。這是開發時最常用的指令之一——資料庫重啟一次要等 30~60 秒,沒必要每改一次程式碼就重來。restart 則不會重新建置映像,改了程式碼不會生效。
09 / 9
關於 Compose 的定位,正確的是?
A 它是 Kubernetes 的替代品,可用於大規模正式環境
B 它是單機工具:適合本機開發、CI 整合測試、小型單機部署;但沒有跨機器調度、自動故障轉移與滾動更新,正式環境的大規模部署需要 Kubernetes
C 它只能用於執行單一容器
D 它會自動把服務部署到雲端
解析
Compose 的價值在宣告式、可進版控、自動處理網路與 volume 關聯。但所有容器都在同一台機器上——一旦這台機器掛了,整套服務就沒了。跨機器的調度與自我修復是 ch14 的主題。

點擊卡片翻面查看答案,共 11 張。

QUESTION
Compose 的三個價值?
點擊翻面
ANSWER
① 宣告式:描述最終狀態,工具負責達成
② 可進版控:整套環境變成可審查的檔案
③ 自動處理網路、volume、啟動順序
(本質只是把一長串 docker run 寫成檔案)
點擊翻回
QUESTION
Compose 的「專案」概念?
點擊翻面
ANSWER
專案名預設 = compose 檔所在的目錄名
所有資源加前綴:myapp-sqlserver-1、myapp_default、myapp_mssql-data
→ 改目錄名 = 換一套全新的 volume(資料看似消失)
點擊翻回
QUESTION
depends_on 的真相?
點擊翻面
ANSWER
只保證容器「被啟動」(幾百毫秒)
不保證服務「可以接受連線」(SQL Server 要 20~60 秒)
症狀:第一次啟動失敗、手動重啟就好
點擊翻回
QUESTION
depends_on 的三種 condition?
點擊翻面
ANSWER
service_started:只等啟動(預設)
service_healthy:等 healthcheck 通過 ✅ Compose 層正解
service_completed_successfully:等一次性任務成功結束(DB 遷移)
點擊翻回
QUESTION
為什麼應用層重試才是真正的正解?
點擊翻面
ANSWER
正式環境沒有 compose 排順序(K8s 不保證 Pod 順序)
運行期也可能短暫斷線(DB 重啟、故障轉移)
→ healthcheck 是體驗優化,應用層重試是正確性保證
點擊翻回
QUESTION
.env 檔到底給誰用?
點擊翻面
ANSWER
給「compose 檔本身」做 ${VAR} 展開
⚠️ 不會自動送進容器!
要進容器必須寫在 environment: 或 env_file:
點擊翻回
QUESTION
環境變數優先序(高到低)?
點擊翻面
ANSWER
① 指令列 -e
② compose 的 environment:
③ env_file:
④ Dockerfile 的 ENV
點擊翻回
QUESTION
三個實用的變數語法?
點擊翻面
ANSWER
${VAR:-預設值}:沒設定就用預設
${VAR:?錯誤訊息}:沒設定就報錯中止(強制必填)
$$VAR:逃脫,原樣傳給容器內的 shell
點擊翻回
QUESTION
多檔覆蓋的合併陷阱?
點擊翻面
ANSWER
純量值(字串/數字):覆蓋 ✅
清單(ports/volumes):預設「相加」不是取代 ⚠️
→ 不確定就先跑 docker compose config
點擊翻回
QUESTION
profiles 用來做什麼?
點擊翻面
ANSWER
把非必要的服務標記起來,預設不啟動
profiles: ['tools'] → docker compose --profile tools up
適合 adminer、測試執行器這類可選工具
點擊翻回
QUESTION
開發最常用的兩個指令?
點擊翻面
ANSWER
docker compose up -d --build --no-deps app
→ 只重建重啟 app,不動資料庫
docker compose config
→ 「設定沒生效」的第一個排查動作
點擊翻回