🎯 重點摘要: Docker Compose 是用一個 YAML 檔定義並管理多容器應用的工具:
  ① 一檔定義全家:web、資料庫、快取、反向代理全寫在 compose.yaml,一條指令整組啟停
  ② v2 已內建:現在是 Docker CLI 的子指令 docker compose(沒有連字號),舊的 python 版 docker-compose 已淘汰
  ③ 啟動順序可控:depends_onhealthcheck 能等資料庫「真正就緒」才啟動 web,不再連線被拒
  ④ 開發體驗起飛:Profiles 按需開關服務群組;Compose Watch 讓程式碼改完自動同步進容器
  ⑤ 環境分離靠覆蓋:基底檔 + compose.override.yaml(開發)+ compose.prod.yaml(正式)各管各的

Docker Compose 是什麼?

真實世界的應用很少只有一個容器——web 後面通常還有資料庫、快取、訊息佇列、反向代理。用裸 docker run 一個一個起,參數長到懷疑人生,網路要手接、順序要手顧、改一行要重打十行。Compose 把這一切收斂成一個 YAML 檔:宣告每個服務用什麼映像、掛什麼磁碟、開什麼埠、誰依賴誰,然後 docker compose up 一鍵整組拉起來。

還沒熟習單容器的話,先看我們的 Docker 容器化指南再回來。

v1 與 v2:別再用舊的了

舊版 docker-compose (v1)現行 docker compose (v2)
型態獨立 Python 套件Docker CLI 內建 plugin(Go 寫的)
指令docker-compose up(有連字號)docker compose up(空格)
維護狀態已停止維護持續更新,新功能都在這

確認版本很簡單:

$ docker compose version
Docker Compose version v2.39.2

安裝

  • macOS / Windows:裝 Docker Desktop 就內建,什麼都不用做
  • Linux:跟著 Docker Engine 一起裝 plugin:
sudo apt-get update
sudo apt-get install docker-compose-plugin
檔名慣例:官方推薦 compose.yamldocker-compose.yml 也認得。放在專案根目錄,之後所有 compose 指令都會自動抓它。

第一個 Compose:三分鐘上手

經典入門場景:一個計數器 web + Redis。新建 compose.yaml

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      - redis
  redis:
    image: redis:7-alpine

然後:

docker compose up -d     # 背景啟動整組服務
curl localhost:8000      # 驗證
docker compose down      # 整組收掉

就這樣。Compose 自動建了一個專屬 network 讓 web 用主機名稱 redis 直接連到 Redis——不需要手動 docker network create、不需要寫 IP。

核心語法詳解

image 與 build

services:
  api:
    image: myapp/api:1.4        # 直接用現成映像
  worker:
    build:                      # 或從本地 Dockerfile 建
      context: ./worker
      dockerfile: Dockerfile
      args:
        NODE_ENV: development
    image: myapp/worker:dev     # 順便命名建出來的映像

ports vs expose

  • ports: 映射到宿主機("8000:5000" 主機 8000 → 容器 5000),給人類和外部流量用
  • expose: 只在同一 network 內聲明埠,不映射出去——純文件用途,服務間互連其實根本不用宣告也能連
⚠️ 埠字串要加引號:"8000:5000" 寫成 8000:5000 在某些 YAML 解析情境會被當成六十進位數字出包,養成加引號的習慣。

volumes 三種掛法

類型寫法用途
Named volumepgdata:/var/lib/postgresql/data資料庫等需要持久化的資料,由 Docker 管理
Bind mount./src:/app/src本地開發即時同步程式碼進容器
Anonymous / tmpfs/tmp拋棄式暫存

Named volume 要在頂層 volumes: 區塊聲明:

services:
  db:
    image: postgres:17
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata:

environment 與變數替換

services:
  api:
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
      REDIS_HOST: cache
    env_file:
      - ./api.env          # 整包變數檔,適合塞一堆設定

${DB_PASSWORD}從 Compose 所在目錄的 .env 檔讀出來做字串替換——注意這是「寫進 YAML 前」的替換,跟容器的環境變數是兩回事。.env 記得放進 .gitignore

restart 政策

行為
no預設,掛了不自動重啟(YAML 裡要加引號 "no",不然會被解析成 false!)
always永遠重啟,含宿主機開機後
unless-stopped同 always,但手動 stop 後就不自動拉起(伺服器最常用的選擇)
on-failure只在非零退出時重啟,可加上限 on-failure:5

networks

預設 Compose 會建一個讓所有服務互通的 network。要隔離(例如資料庫不想被前端層摸到)就自訂:

services:
  nginx:
    networks: [frontend]
  api:
    networks: [frontend, backend]
  db:
    networks: [backend]      # 只在 backend 網段,nginx 連不到它
networks:
  frontend:
  backend:
    internal: true           # 完全不上外網

啟動順序:depends_on × healthcheck

新手最常撞牆的一課:depends_on 只保證「容器啟動了」,不保證裡面的 PostgreSQL「接受連線了」。web 秒啟動、db 還在初始化,連線直接被拒。

解法是讓 db 自己報告健康狀態,web 等「健康」而不是等「活著」:

services:
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy   # 等健康檢查通過
        restart: true                # db 重啟時跟著重啟 web
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s       # 每 10 秒測一次
      timeout: 5s         # 單次超過 5 秒算失敗
      retries: 5          # 連續失敗 5 次判 unhealthy
      start_period: 30s   # 前 30 秒的失敗不計入(初始化寬限)
condition意義
service_started容器啟動就好(等同舊版的 depends_on)
service_healthy健康檢查通過才算
service_completed_successfully適合一次性 job(如 migration 容器),跑完且成功才繼續
💡 觀察健康狀態:docker compose ps 會顯示 (healthy)/(unhealthy);想看細節用 docker inspect --format '{{.State.Health.Status}}' <容器名>

常用指令速查表

指令作用
docker compose up -d背景建立並啟動全部服務(--build 強制重建映像)
docker compose down停止並移除容器與 network;-v 連 volume 一起刪(資料會消失!)
docker compose ps列出本專案容器狀態與健康情況
docker compose logs -f api追蹤某服務日誌(不加服務名就是全部)
docker compose exec api sh進入運行中容器開 shell
docker compose build只重建有 build 定義的映像
docker compose pull拉取最新映像
docker compose restart api重啟指定服務
docker compose config把最終合併後的完整配置印出來驗證(除錯神器)
docker compose top看各容器內的行程
docker compose cp api:/app/log.txt .容器與宿主機之間複製檔案

Profiles:按需啟動的服務群組

有些服務只在特定情境需要——本地的郵件測試工具、除錯面板、GPU 推理服務。給它標上 profile,平時 up 不會啟動:

services:
  api:
    image: myapp/api            # 沒有 profiles = 永遠啟動
  mailpit:
    image: axllent/mailpit
    profiles: ["devtools"]      # 只有啟用 devtools 才會跑
  k6:
    image: grafana/k6
    profiles: ["loadtest"]
docker compose --profile devtools up -d              # 啟用單一群組
docker compose --profile devtools --profile loadtest up -d   # 同時多個
COMPOSE_PROFILES=devtools docker compose up -d       # 用環境變數也行
⚠️ Profiles 不是安全邊界——它只是「預設不啟動」的開關,明確點名服務(docker compose run mailpit sh)照樣能啟動它。別拿來藏不該跑的東西。

Compose Watch:改 code 不用 rebuild

v2.22 加入的殺手級開發功能。在服務下宣告 develop.watch 規則,然後 docker compose up --watch,檔案一存就依規則處理:

services:
  web:
    build: .
    develop:
      watch:
        - action: sync            # 原地同步,配框架熱重載(React/Vite/Django)
          path: ./src
          target: /app/src
          ignore: [node_modules/]
        - action: rebuild         # 依賴變了就重建映像
          path: package.json
        - action: sync+restart    # 同步後重啟,適合 nginx.conf 這類設定
          path: ./nginx.conf
          target: /etc/nginx/nginx.conf
action觸發時行為適用
sync直接把檔案同步進運行中的容器Python/JS 等直譯語言+熱重載
rebuild重建映像並重建容器Go/Rust/Java 等編譯語言
sync+restart同步後重啟容器設定檔修改
sync+exec同步後在容器內執行指令同步後跑 migration/重新載入

經驗法則:直譯語言用 sync、編譯語言用 rebuild、設定檔用 sync+restart,而且只對需要的服務開 watch。

多檔案配置:一套 YAML 走天下

開發和正式環境的需求天生不同(bind mount vs 穩定映像、暴露 debug 埠 vs 不暴露)。官方約定的分層做法:

  • compose.yaml —— 共同基底(服務清單、networks、volumes)
  • compose.override.yaml —— 開發差異;up 時自動載入
  • compose.prod.yaml —— 正式差異;明確指定載入
# 開發(自動疊 override)
docker compose up -d

# 正式(只用基底 + prod)
docker compose -f compose.yaml -f compose.prod.yaml up -d

# 疊完到底長怎樣?先看再跑
docker compose -f compose.yaml -f compose.prod.yaml config

同名欄位是「覆蓋」而非合併(ports、environment 這類列表會以後載入者為準)。更大的專案還能用頂層 include: 把多個 compose 檔組織成一棵樹。

實戰範例:四服務架構

把前面學的全部拼起來——Nginx 反代 + API + PostgreSQL + Redis:

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped

  api:
    build: ./api
    expose:
      - "3000"
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
      REDIS_URL: redis://cache:6379/0
    env_file:
      - ./api.secrets.env
    develop:
      watch:
        - action: sync
          path: ./api/src
          target: /app/src
          ignore: ["**/__pycache__/"]
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped
    networks: [frontend, backend]

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped
    networks: [backend]

  cache:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    restart: unless-stopped
    networks: [backend]

networks:
  frontend:
  backend:

volumes:
  pgdata:

這份配置做到了:資料庫只在 backend 網段外界摸不到、API 健康後 Nginx 才啟動、資料存在 named volume 重啟不丟、開發時 --watch 即時同步原始碼。

最佳實務與常見坑

  1. down -v 是核彈:它會刪掉 named volumes,本地資料庫直接歸零。日常請用 down,確定要重置資料再 -v
  2. healthcheck 只管啟動順序:db 中途掛掉不會自動重啟你的 api,應用層仍要有連線重試邏輯
  3. 密碼不要寫死在 YAML:.envenv_file,正式環境換 secrets 管理方案
  4. 改了 yaml 沒生效?Compose 不會自動偵測配置變更,要 up -d 重新套用;改 Dockerfile 要 up -d --build
  5. 孤兒容器:改過 service 名字後舊容器可能還留著,看到警告加 --remove-orphans
  6. configup多檔案覆蓋疊完的結果常常跟想像不同,docker compose config 看最終形狀再執行

常見問題 FAQ

docker compose 和 docker-compose 有差嗎?

指令幾乎完全相容,但 v1(連字號版)已停止維護,新功能(watch、profiles 完整語義、include)都只在 v2。現在寫教學、腳本一律用 docker compose

可以像 swarm 一樣 scale 嗎?

無狀態服務可以 docker compose up -d --scale api=3,但要注意埠衝突(用 expose 別用 ports)且這只是本機多開,不是生產級編排——那種需求請上 Kubernetes。

資料要怎麼備份?

named volume 的內容可以用 docker run --rm -v 專案名_pgdata:/data -v $PWD:/backup alpine tar czf /backup/pgdata.tgz /data 打包出來,或直接對 db 容器下 pg_dump。

up 的時候說 port already in use?

宿主機的埠被占了(可能是上一份沒關乾淨的專案或系統服務)。lsof -i :8000 找出兇手,或換一個映射埠。

CI/CD 裡怎麼用?

GitHub Actions 的 runner 有內建 Docker Compose,可以直接 docker compose up -d db 起測試資料庫、跑完 down 收掉。搭配 profiles 可以讓 CI 跳過用不到的重型服務。

總結

Compose 的價值在於把「我的環境怎麼跑」變成一份可版本控制的宣言:新人 clone 下來 docker compose up 就能跑,正式環境同一份檔案換個 overlay 就上線。掌握三件事就算畢業:services/volumes/networks 的宣告式語法、healthcheck × depends_on 的就緒控制、override 分層的環境分離。之後再把 Profiles 和 Compose Watch 加進日常流程,開發體驗會徹底不一樣。

🎯 快速上手:專案根目錄寫一份最小 compose.yamldocker compose up -ddocker compose logs -f 看日誌 → docker compose down 收工。先跑通兩個服務互連,再慢慢補 healthcheck 和多環境,一天就能內化。