WorkBlog

Docker Compose 多容器部署教學:從入門到實戰

趙柏翰
Docker Compose 多容器部署教學:從入門到實戰

還記得第一次用 Docker 部署應用的時候嗎?一個容器跑一個服務,感覺挺簡單的。但等到你的專案開始長大,需要資料庫、快取、訊息佇列、反向代理……一堆服務要同時跑的時候,手動管理每個容器就變成了一場噩夢。這就是 Docker Compose 存在的意義——它讓你用一個 YAML 檔案就能定義和管理整個多容器應用。

為什麼需要 Docker Compose

我先說個真實的故事。之前在一個專案中,我需要同時跑 Node.js API 伺服器、PostgreSQL 資料庫、Redis 快取和 Nginx 反向代理。一開始我用四個獨立的 docker run 指令來啟動它們,每次重啟都要記住一堆參數,還要手動設定網路讓它們能互相通訊。有一次不小心打錯參數,整個環境就壞了,花了快一個小時才修好。

後來改用 Docker Compose 之後,所有設定都寫在一個 docker-compose.yml 檔案裡,一個 docker compose up 指令就能把所有服務跑起來。要停掉的時候,docker compose down 一秒搞定。再也不用記那些複雜的參數了。

除了方便之外,Docker Compose 還有幾個重要的優勢。第一,它提供了一致的開發環境,團隊裡每個人拿到同一份 docker-compose.yml 就能跑出一模一樣的環境。第二,它自動幫你建立網路,同一個 Compose 檔案裡的服務可以直接用服務名稱互相存取。第三,它支援服務之間的相依關係設定,確保服務按照正確的順序啟動。

docker-compose.yml 基本結構

一個 docker-compose.yml 檔案的基本結構其實很直覺。讓我們從最簡單的範例開始。

version: '3.9'

services:
  web:
    image: node:20-alpine
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
    environment:
      - NODE_ENV=production
    depends_on:
      - db

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=secret123

volumes:
  pgdata:

這個範例定義了兩個服務:一個 Node.js 的 web 服務和一個 PostgreSQL 的 db 服務。depends_on 告訴 Compose,web 服務需要等 db 服務啟動後才能啟動。底下的 volumes 區塊定義了一個具名的 volume,用來持久化資料庫的資料。

不過這裡有一個常見的陷阱要注意:depends_on 只會等容器「啟動」,不會等服務「準備好」。也就是說,db 容器啟動了不代表 PostgreSQL 已經可以接受連線。這個問題我們後面會用 health checks 來解決。

服務定義與映像檔設定

在服務定義中,你可以選擇使用現成的映像檔,或者用 Dockerfile 來建立自己的映像檔。實務上兩種方式經常混用——自己開發的服務用 Dockerfile,第三方的服務(像資料庫、快取)直接用官方映像檔。

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
      args:
        - BUILD_ENV=production
    ports:
      - "8080:8080"
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    restart: always

restart 策略也很重要。unless-stopped 表示除非你手動停止,否則容器掛了會自動重啟。always 則是不管什麼情況都重啟。對於生產環境,我通常建議對核心服務用 always,非核心服務用 unless-stopped。如果你的 API 服務有遵循REST API 設計最佳實踐,那搭配適當的 restart 策略可以確保服務的高可用性。

Networks 網路配置

Docker Compose 預設會為整個專案建立一個 bridge 網路,所有服務都能透過服務名稱互相存取。但在比較複雜的架構中,你可能會想把不同的服務隔離到不同的網路。

services:
  web:
    networks:
      - frontend
      - backend

  api:
    networks:
      - backend

  db:
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

在這個例子中,web 服務同時連接 frontend 和 backend 兩個網路,所以它可以跟外部世界通訊,也可以跟 api 和 db 通訊。但 db 只在 backend 網路中,外部完全無法直接存取它。internal: true 的設定更進一步禁止了 backend 網路的對外連線,提供了額外的安全性。

網路隔離在安全性上非常重要。你不會希望資料庫直接暴露在外部網路上,對吧?如果你在處理身份驗證相關的服務,搭配JWT 身份驗證教學可以讓你的微服務架構更加安全。

Volumes 資料持久化

容器本身是暫時的——當你刪除容器後,裡面的資料就消失了。如果你不想每次重啟都遺失資料庫的資料,就需要使用 volumes。

services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro

  app:
    volumes:
      - ./src:/app/src
      - /app/node_modules

volumes:
  pgdata:
    driver: local

這裡有兩種不同類型的 volume。pgdata:/var/lib/postgresql/data 是具名 volume,Docker 會自動管理它的儲存位置。./src:/app/src 是綁定掛載(bind mount),直接把主機上的目錄對應到容器裡。

開發的時候,bind mount 特別好用,因為你改了程式碼之後容器裡面立刻就會更新。但在生產環境中,建議用具名 volume 來管理資料,這樣比較安全也比較好管理。注意那個 :ro 後綴,它表示唯讀掛載,可以防止容器意外修改主機上的檔案。如果你在選擇資料庫方案,可以看看PostgreSQL vs MySQL 比較來決定哪個更適合你的專案。

Health Checks 健康檢查

前面提到 depends_on 只會等容器啟動,不會等服務真正準備好。Health checks 就是用來解決這個問題的。

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  api:
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

透過 condition: service_healthy,api 服務會等到 db 的健康檢查通過後才啟動。start_period 給了 PostgreSQL 30 秒的緩衝時間來完成初始化,在這段時間內的健康檢查失敗不會計入重試次數。

我的建議是,每個服務都應該要有 health check。這不只是為了啟動順序,更是為了讓你隨時都能知道服務的狀態。當某個服務不健康的時候,Docker 可以自動重啟它。

環境變數管理

把機密資訊直接寫在 docker-compose.yml 裡面是一個很糟糕的做法。正確的方式是使用 .env 檔案或者外部的 secrets 管理工具。

# .env 檔案
POSTGRES_DB=myapp
POSTGRES_USER=admin
POSTGRES_PASSWORD=super_secret_password
REDIS_URL=redis://redis:6379
API_PORT=8080
services:
  db:
    image: postgres:16
    env_file:
      - .env
    environment:
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}

  api:
    env_file:
      - .env
    ports:
      - "${API_PORT}:${API_PORT}"

記得把 .env 加到 .gitignore 裡面!這一點千萬不能忘記。你可以提供一個 .env.example 檔案,列出需要的環境變數但不包含實際的值,方便其他開發者設定自己的環境。

多階段部署策略

在實務中,你可能需要不同環境(開發、測試、生產)使用不同的配置。Docker Compose 支援多個檔案的覆蓋機制,讓你可以優雅地處理這個需求。

# docker-compose.yml(基礎設定)
services:
  api:
    build: ./api
    environment:
      - NODE_ENV=production

# docker-compose.override.yml(開發環境覆蓋)
services:
  api:
    volumes:
      - ./api/src:/app/src
    environment:
      - NODE_ENV=development
      - DEBUG=true
    ports:
      - "9229:9229"  # debug port

Docker Compose 會自動合併 docker-compose.ymldocker-compose.override.yml。在開發環境中,它會自動使用覆蓋的設定。部署到生產環境時,你可以明確指定只用基礎檔案:docker compose -f docker-compose.yml -f docker-compose.prod.yml up

2026 年最佳實踐

隨著 Docker 生態系統的持續發展,2026 年有一些新的最佳實踐值得關注。

首先,Docker Compose V2 現在已經是預設版本,指令從 docker-compose 變成了 docker compose(注意是空格不是連字號)。如果你還在用 V1,強烈建議升級。

其次,善用 profiles 來管理不同場景的服務組合。比如,你可能只有在開發的時候才需要跑 Mailhog 這種郵件測試工具,就可以用 profile 來控制。

services:
  mailhog:
    image: mailhog/mailhog
    profiles:
      - dev
    ports:
      - "8025:8025"

啟動的時候用 docker compose --profile dev up 就能包含這些開發專用的服務。

另外,現在 Docker 對 Apple Silicon(M 系列晶片)的支援已經非常成熟了。如果你的團隊中有人用 ARM 架構的機器,記得在 build 的時候指定 platform,或者使用 multi-arch 映像檔,確保跨平台相容性。如果你的前端專案用到了最新的框架,可以參考React 19 新功能教學,搭配 Docker Compose 打造完整的全端開發環境。

常見問題與解決方案

最後來聊聊一些我和身邊的開發者朋友們經常遇到的問題。

第一個是連接埠衝突。如果你本機已經有個 PostgreSQL 跑在 5432 埠,Docker 裡的 PostgreSQL 就沒辦法再綁定同一個埠。解決方式是改用不同的主機埠,例如 "5433:5432"

第二個是 volume 權限問題。有時候容器內的使用者和主機的使用者不一樣,會導致檔案讀寫權限的問題。解決方式是在 Dockerfile 中指定正確的使用者,或者使用 user 選項。

第三個是映像檔太大。每次 build 都下載一堆依賴,非常浪費時間和空間。善用 multi-stage build 和 .dockerignore 可以大幅減少映像檔的大小。Alpine 版本的基礎映像檔通常比完整版小很多,優先考慮使用。

Docker Compose 是現代後端開發幾乎必備的工具。掌握了這些知識之後,你應該能夠應付大多數的多容器部署場景。持續練習,遇到問題多查文件,很快你就能成為容器化部署的高手。

趙柏翰

全端工程師,八年開發經驗。熱愛 TypeScript 生態系。

ReactTypeScriptPythonNode.js

你可能也喜歡

探索其他領域的精選好文