Skip to content

3. Docker Compose

3.1 為什麼用 Compose?

  • 多服務協作、可版本化、易分享與啟停
  • 把「一堆 docker run 參數」收斂成一個 compose.yaml,團隊協作更一致

3.2 YAML 結構重點

  • servicesbuildimageportsvolumesenvironment
  • depends_onhealthcheckprofiles
  • deploy.resources.limits(Swarm 模式下的資源限制)

3.3 指令速查

docker compose up -d
docker compose ps
docker compose logs -f
docker compose exec app sh
docker compose down -v
# 全域:-f <檔.yml>(多檔合併)、-p <專案名>
````

## 3.4 範例(Web + Postgres + Redis)

```yaml
services:
  app:
    build: .
    ports: ["8080:8080"]
    env_file: .env
    depends_on: [db, cache]

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    volumes:
      - pgdata:/var/lib/postgresql/data

  cache:
    image: redis:7

volumes:
  pgdata: {}

3.5 環境變數與 .env:預設行為與注入方式

A) 兩個不同概念

  • 插值(interpolation):在 compose.yaml${VAR} 取代值(image tag、port、路徑)。

  • 預設:Compose 會讀與 compose.yaml 同層的 .env 來做插值。

  • 覆蓋:可用 docker compose --env-file <檔案> 指定其他 .env;可指定多個,後者覆蓋前者。
  • 優先序(同名時):Shell 環境 > 本地 .env(預設)> --env-file 指定的檔案(後者覆蓋前者)
  • 容器環境(Container Env):要讓容器真的拿到變數,必須寫 environment:env_file:只放 .env 只會做插值,不會自動進容器

B) 基本範例

.env

TAG=v1.5
APP_PORT=8080

compose.yaml(插值 + 注入)

services:
  web:
    image: "webapp:${TAG}"        # → webapp:v1.5
    ports: ["${APP_PORT}:8080"]   # → 主機 8080 對容器 8080
    environment:
      - APP_PORT=${APP_PORT}      # 注入容器
    env_file:
      - .env                      # 批次注入

檢視解析結果:

docker compose config

C) 使用不同或多個 .env

docker compose --env-file ./config/.env.dev up -d
docker compose --env-file .env --env-file .env.override config

D) 注意事項

  • docker run 不會自動讀 .env;要用 --env-file-e KEY=VAL
  • .env 插值是 Compose CLI 的行為;docker stack deploy(Swarm)不支援這種插值。

3.6 進階技巧

  • 多檔合併:docker compose -f compose.yaml -f compose.prod.yaml up -d
  • 專案名稱:-p myproj
  • 健康檢查/相依就緒:healthcheck + 應用層重試
  • 資源限制(Swarm):deploy.resources.limits
---

## `examples/` 目錄與檔案

### 結構

examples/ ├── flask-postgres/ │ ├── app.py │ ├── requirements.txt │ ├── Dockerfile │ └── docker-compose.yml └── static-nginx/ ├── public/ │ └── index.html ├── package.json ├── Dockerfile └── docker-compose.yml

### `examples/flask-postgres/app.py`
```python
from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.get("/")
def hello():
    return "Hello from Flask in Docker! 👋"

@app.get("/env")
def envs():
    keys = ["DB_HOST", "DB_USER", "DB_PASSWORD"]
    return jsonify({k: os.getenv(k, "") for k in keys})

@app.get("/health")
def health():
    return "OK"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

examples/flask-postgres/requirements.txt

flask==3.0.3

examples/flask-postgres/Dockerfile

FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER 1000
EXPOSE 8080
CMD ["python", "app.py"]

examples/flask-postgres/docker-compose.yml

services:
  web:
    build: .
    ports: ["8080:8080"]
    environment:
      - DB_HOST=db
      - DB_USER=postgres
      - DB_PASSWORD=example
    depends_on: [db]

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    volumes:
      - pg:/var/lib/postgresql/data

volumes:
  pg: {}

examples/static-nginx/public/index.html

<!doctype html>
<html lang="zh-Hant">
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Static with Nginx (Multi-stage)</title>
  <h1>🚀 Hello from Nginx!</h1>
  <p>這是經由 Node 階段產生 <code>dist/</code>,再由 Nginx 提供的靜態頁。</p>
</html>

examples/static-nginx/package.json

{
  "name": "static-site",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build": "mkdir -p dist && cp -r public/* dist/"
  }
}

examples/static-nginx/Dockerfile

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install --no-audit --no-fund
COPY public ./public
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
USER 101
EXPOSE 80
CMD ["nginx","-g","daemon off;"]

examples/static-nginx/docker-compose.yml

services:
  web:
    build: .
    ports: ["8080:80"]