cover

Docker Compose 實戰模板:不要每次都從零開始

每次開新專案都在重新寫 docker-compose.yml?來,這裡有一套即用範本。

當團隊開始有三個以上的專案,你會發現大家的 docker-compose.yml 長得都差不多——但又不完全一樣。有人忘了設 health check,有人沒限制 log 大小結果磁碟爆掉,有人的 volume 命名完全隨機讓維運人員找不到資料在哪。標準化的 compose 模板不是為了限制彈性,而是為了讓 80% 的常見場景不需要思考就能正確部署,把精力留給那 20% 需要客製化的部分。

四套模板的定位與連接方式

flowchart TD
    subgraph DevEnv["模板 4: 開發環境"]
        DevDB[(PostgreSQL)]
        DevRedis[(Redis)]
        DevMinio[(MinIO)]
        DevMail[(Mailhog)]
    end

    subgraph AppStack["模板 1: Web App Stack"]
        Nginx[Nginx<br/>Reverse Proxy]
        App[Application<br/>API Server]
        DB[(PostgreSQL)]
        Cache[(Redis)]
    end

    subgraph MonStack["模板 2: 監控 Stack"]
        Prom[Prometheus]
        Grafana[Grafana]
        NodeExp[Node Exporter]
    end

    subgraph LogStack["模板 3: 日誌 Stack"]
        ES[(Elasticsearch)]
        Filebeat[Filebeat]
        Kibana[Kibana]
    end

    AppStack -->|metrics endpoint| MonStack
    AppStack -->|container logs| LogStack
    MonStack -->|self metrics| LogStack

    style DevEnv fill:#e8f5e9,stroke:#388e3c
    style AppStack fill:#e3f2fd,stroke:#1976d2
    style MonStack fill:#fff3e0,stroke:#f57c00
    style LogStack fill:#fce4ec,stroke:#c62828

開發環境用模板 4 在本機跑依賴服務,部署時用模板 1 跑正式的 App Stack,再疊加模板 2 和 3 做監控與日誌收集。四套模板各自獨立但可以透過 Docker network 互相對接。


模板 1: Web App Stack(Nginx + App + PostgreSQL + Redis)

這是最常見的 Web 應用架構。Nginx 做反向代理和靜態檔案服務、App 跑業務邏輯、PostgreSQL 存持久資料、Redis 做快取和 session 儲存。

# docker-compose.app.yml
version: "3.8"
 
services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - static_files:/var/www/static:ro
    depends_on:
      app:
        condition: service_healthy
    networks:
      - frontend
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3
 
  app:
    image: ${APP_IMAGE:-myapp:latest}
    env_file:
      - .env
    environment:
      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
      - REDIS_URL=redis://cache:6379/0
    volumes:
      - static_files:/app/static
      - upload_data:/app/uploads
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    networks:
      - frontend
      - backend
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 30s
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    networks:
      - backend
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
 
  cache:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    networks:
      - backend
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "3"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 300M
 
volumes:
  pg_data:
    name: ${PROJECT_NAME:-myapp}_pg_data
  redis_data:
    name: ${PROJECT_NAME:-myapp}_redis_data
  static_files:
    name: ${PROJECT_NAME:-myapp}_static
  upload_data:
    name: ${PROJECT_NAME:-myapp}_uploads
 
networks:
  frontend:
    name: ${PROJECT_NAME:-myapp}_frontend
  backend:
    name: ${PROJECT_NAME:-myapp}_backend
    internal: true

為什麼這樣配?

  • Network 分離frontend 網路給需要外部存取的服務(Nginx、App),backend 標記為 internal: true,只有 App 能連到 DB 和 Redis,Nginx 不能直接碰資料庫。這是最基本的網路隔離,對應 Network & DNS 提到的網段切分概念。
  • Health check:每個服務都有 health check,depends_on 搭配 condition: service_healthy 確保啟動順序正確。沒有 health check 的 depends_on 只保證容器啟動,不保證服務就緒。
  • Resource limits:用 deploy.resources 限制每個容器的 CPU 和記憶體上限,避免某個服務吃掉所有資源導致其他服務被 OOM Kill。
  • Named volumes:用 ${PROJECT_NAME} 前綴命名,多個專案在同一台 Host 上不會 volume 名稱衝突。

模板 2: 監控 Stack(Prometheus + Grafana + Node Exporter)

搭配 Metrics & Monitoring 使用。這套模板收集 Host 層級和容器層級的指標,透過 Grafana 視覺化。

# docker-compose.monitoring.yml
version: "3.8"
 
services:
  prometheus:
    image: prom/prometheus:v2.48.0
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=30d"
      - "--web.enable-lifecycle"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/rules:/etc/prometheus/rules:ro
      - prom_data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - monitoring
      - backend
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 2G
 
  grafana:
    image: grafana/grafana:10.2.0
    environment:
      GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
      GF_INSTALL_PLUGINS: grafana-clock-panel,grafana-piechart-panel
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    ports:
      - "3000:3000"
    depends_on:
      prometheus:
        condition: service_healthy
    networks:
      - monitoring
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 5s
      retries: 3
 
  node-exporter:
    image: prom/node-exporter:v1.7.0
    command:
      - "--path.procfs=/host/proc"
      - "--path.rootfs=/rootfs"
      - "--path.sysfs=/host/sys"
      - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    ports:
      - "9100:9100"
    networks:
      - monitoring
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "0.2"
          memory: 128M
 
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.47.2
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports:
      - "8080:8080"
    networks:
      - monitoring
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "0.3"
          memory: 256M
 
volumes:
  prom_data:
    name: monitoring_prom_data
  grafana_data:
    name: monitoring_grafana_data
 
networks:
  monitoring:
    name: monitoring
  backend:
    external: true
    name: ${PROJECT_NAME:-myapp}_backend

與 App Stack 對接:監控 Stack 的 Prometheus 加入 App Stack 的 backend 網路(宣告為 external),就能 scrape App 的 /metrics endpoint。你不需要把 App 的 port 暴露到 Host 上,Prometheus 透過 Docker 內部網路直接存取。


模板 3: 日誌 Stack(Elasticsearch + Filebeat + Kibana)

對應 Log Management 的 EFK/ELK 架構。Filebeat 收集容器日誌,送到 Elasticsearch 儲存,Kibana 提供搜尋和分析 UI。

# docker-compose.logging.yml
version: "3.8"
 
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - cluster.name=logging-cluster
    volumes:
      - es_data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    networks:
      - logging
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 1G
 
  filebeat:
    image: docker.elastic.co/beats/filebeat:8.11.0
    user: root
    command: filebeat -e -strict.perms=false
    volumes:
      - ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - filebeat_data:/usr/share/filebeat/data
    depends_on:
      elasticsearch:
        condition: service_healthy
    networks:
      - logging
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 256M
 
  kibana:
    image: docker.elastic.co/kibana/kibana:8.11.0
    environment:
      ELASTICSEARCH_HOSTS: '["http://elasticsearch:9200"]'
    ports:
      - "5601:5601"
    depends_on:
      elasticsearch:
        condition: service_healthy
    networks:
      - logging
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:5601/api/status || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 120s
 
volumes:
  es_data:
    name: logging_es_data
  filebeat_data:
    name: logging_filebeat_data
 
networks:
  logging:
    name: logging

Filebeat 掛載 Docker 的 container log 目錄和 docker.sock,自動發現所有容器並收集日誌。比 Fluentd 更輕量,適合單機環境。如果需要更複雜的日誌處理(解析、轉換、路由),再考慮換成 Fluentd 或 Logstash。


模板 4: 開發環境(PostgreSQL + Redis + MinIO + Mailhog)

開發者不需要在本機安裝這些服務。一個 docker compose up -d 就把所有依賴跑起來,用完 docker compose down 清掉。乾淨、可重複、不污染本機環境。

# docker-compose.dev.yml
version: "3.8"
 
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: myapp_dev
    ports:
      - "5432:5432"
    volumes:
      - dev_pg_data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 5s
      timeout: 3s
      retries: 5
 
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - dev_redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3
 
  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - dev_minio_data:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 10s
      timeout: 5s
      retries: 3
 
  mailhog:
    image: mailhog/mailhog:latest
    ports:
      - "1025:1025"
      - "8025:8025"
 
volumes:
  dev_pg_data:
  dev_redis_data:
  dev_minio_data:

MinIO 提供 S3 相容的物件儲存(對應 Storage Management),開發時不需要連上真正的 AWS S3。Mailhog 攔截所有寄出的 email 並提供 Web UI 檢視,開發時不怕誤寄信給真實使用者。Port 都直接映射出來,方便開發工具連接。


共通最佳實踐

.env 變數管理

把會變動的值(密碼、映像版本、專案名稱)都抽到 .env 檔:

# .env
PROJECT_NAME=myapp
APP_IMAGE=myapp:v1.2.3
 
DB_USER=appuser
DB_PASSWORD=strong-password-here
DB_NAME=myapp_production
 
GRAFANA_PASSWORD=another-strong-password
  • .env 不進 Git(加到 .gitignore
  • 提供 .env.example 列出所有需要的變數和預設值
  • 參考 Secrets & Config 的密碼管理策略

Volume 策略:Named Volumes vs Bind Mounts

flowchart LR
    subgraph Named["Named Volumes"]
        NV1["pg_data"]
        NV2["redis_data"]
        NV3["es_data"]
    end

    subgraph Bind["Bind Mounts"]
        BM1["./nginx/conf.d"]
        BM2["./prometheus/prometheus.yml"]
        BM3["./db/init"]
    end

    Named -->|用途| Data["持久化資料<br/>由容器管理<br/>不需要直接編輯"]
    Bind -->|用途| Config["設定檔<br/>由開發者管理<br/>需要頻繁修改"]
  • Named volumes:給資料庫、快取、Prometheus 等需要持久化的資料。Docker 管理存放位置,備份用 docker volume 命令操作。
  • Bind mounts:給設定檔、初始化腳本、SSL 憑證。檔案在 Host 上直接編輯,容器讀取。記得加 :ro(read-only)防止容器意外修改。

Network 設計

flowchart TD
    Internet((Internet)) --> Nginx

    subgraph frontend["frontend network"]
        Nginx[Nginx]
        App[App]
    end

    subgraph backend["backend network (internal)"]
        App2[App]
        DB[(PostgreSQL)]
        Cache[(Redis)]
    end

    subgraph monitoring["monitoring network"]
        Prom[Prometheus]
        Grafana[Grafana]
    end

    App --- App2
    Prom -.->|scrape /metrics| App2

    style backend fill:#ffe0e0,stroke:#c62828
    style frontend fill:#e0f0ff,stroke:#1976d2
    style monitoring fill:#fff3e0,stroke:#f57c00
  • frontend:面向外部的服務(Nginx、App)
  • backend:內部服務(DB、Redis),設為 internal: true 阻止外部存取
  • monitoring:監控服務獨立網路,需要 scrape App 時加入 backend network
  • 原則:每個服務只加入它需要的網路,最小權限

Health Check 配置

每個服務都應該有 health check。沒有 health check 的容器,Docker 只知道「process 在跑」,不知道「服務是否正常回應」。

# 常見 health check 範例
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  interval: 15s     # 每 15 秒檢查一次
  timeout: 5s       # 5 秒內沒回應就算失敗
  retries: 3        # 連續 3 次失敗才標記為 unhealthy
  start_period: 30s # 啟動後等 30 秒再開始檢查(給服務啟動時間)

Resource Limits

deploy:
  resources:
    limits:
      cpus: "1.0"
      memory: 512M
    reservations:
      cpus: "0.25"
      memory: 128M
  • limits:硬上限,超過就被限制(CPU throttle)或殺掉(OOM kill)
  • reservations:軟下限,Docker 排程時保證至少有這麼多資源
  • 一定要設 memory limit,不然一個 memory leak 可以拖垮整台 Host

Logging Driver

logging:
  driver: json-file
  options:
    max-size: "10m"   # 單個 log 檔案最大 10MB
    max-file: "3"     # 最多保留 3 個檔案

不設定的話,Docker 預設的 json-file driver 不會 rotate,log 會一直長大直到磁碟滿。這是新手最常踩到的坑之一。或者在 Container Runtimedaemon.json 設定全域預設值。


延伸閱讀