3. Docker Compose
3.1 為什麼用 Compose?
- 多服務協作、可版本化、易分享與啟停
- 把「一堆
docker run參數」收斂成一個compose.yaml,團隊協作更一致
3.2 YAML 結構重點
services、build、image、ports、volumes、environmentdepends_on、healthcheck、profilesdeploy.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
compose.yaml(插值 + 注入)
services:
web:
image: "webapp:${TAG}" # → webapp:v1.5
ports: ["${APP_PORT}:8080"] # → 主機 8080 對容器 8080
environment:
- APP_PORT=${APP_PORT} # 注入容器
env_file:
- .env # 批次注入
檢視解析結果:
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/ ├── 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
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;"]