你最熟悉的就是這個指令——但也是最黑箱的一個。
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 -ddocker-compose(有連字號,Python 寫的獨立工具)已被 docker compose(無連字號,Go 寫的 Docker CLI 外掛)取代。檔案開頭的 version: '3.8' 也已經過時,現在可以直接省略——新版會忽略它,寫了反而可能收到警告。這個概念沒搞清楚,會遇到一堆詭異現象。
目錄名:myapp/
容器: myapp-sqlserver-1 ← 專案名-服務名-序號
網路: myapp_default ← 自動建立的專屬網路
Volume:myapp_mssql-data ← 具名 volume 也會加前綴myapp_mssql-data 變成 newname_mssql-data → 那是一個全新的空 volume。舊的資料還在,只是沒被掛上。-p 指定不同專案名。external 網路明確共用。# 明確指定專案名(不依賴目錄名,CI 上特別有用)
docker compose -p myproject up -d
# 或寫在檔案裡
name: myproject
services:
...-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 # 直覺:等資料庫好了再啟動 appConnection refused → 崩潰。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。這個細節卡過很多人。service_started(預設,只等啟動)、service_healthy(等健康檢查通過)、service_completed_successfully(等它跑完並成功結束,適合初始化任務)。# 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 檔旁邊,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,容器裡就有這個環境變數——沒有。services:
app:
environment:
SPRING_PROFILES_ACTIVE: dev # 寫死
MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD} # 從 .env 取值再送進去 ✅
DB_PASSWORD: # 只寫名字 = 從 shell 環境繼承services:
app:
env_file:
- ./config/app.env # 這個檔案裡的每一行都會變成容器的環境變數 ✅docker compose run -e KEY=VAL(指令列)environmentenv_file 指定的檔案ENV${VAR:-預設值} # VAR 沒設定或為空時使用預設值
${VAR:?錯誤訊息} # VAR 沒設定就直接報錯中止(強制必填,很好用)
$$VAR # 逃脫:不要展開,原樣傳給容器內的 shelldocker compose config它會印出所有變數展開、所有覆蓋檔合併之後的最終設定。「我明明設定了怎麼沒生效」的第一個排查動作就是這個——不要用猜的。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' # 遠端除錯埠docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# ↑ 基礎 ↑ 後面的覆蓋前面的ports、volumes)預設是「合併相加」——所以你在覆蓋檔寫 ports 不會取代原本的,而是兩個都存在。要真正取代得用 !reset 或重新設計結構。不確定結果時,一律先跑 docker compose config 看合併後長什麼樣。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-runnerdocker 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: [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 | 映像內建的預設值 | 會(優先序最低) | 合理的預設,不放密碼 |
| 指令 | 作用 | 備註 |
|---|---|---|
| up -d --build | 重新建置並在背景啟動 | 改了 Dockerfile 之後用 |
| up -d --no-deps app | 只重啟 app,不動相依服務 | 開發時最常用,不會重啟資料庫 |
| config | 印出變數展開與多檔合併後的最終設定 | 「設定沒生效」的第一個排查動作 |
| logs -f app | 跟隨單一服務日誌 | 加 --tail 100 只看最近 |
| ps | 看狀態 | 會顯示 healthy/unhealthy |
| down | 停止並移除容器與網路 | volume 保留 ✅ |
| down -v | 連 volume 一併刪除 | 💣 資料全毀,無確認提示 |
點擊卡片翻面查看答案,共 11 張。