requirements.txt 的十幾個套件DATABASE_URL、SECRET_KEY、OLLAMA_URL……test.db 會建在啟動目錄(Day 15)今天把這些全部打包進一個 Docker 映像檔。同一個映像檔可以啟動 Web API,也可以直接執行 CLI。
第 4 週:發佈與開源
讀完這篇後,你會完成:
Dockerfile:Python 3.12、中文字型、非 root 使用者、健康檢查.dockerignore:不把虛擬環境、資料庫、.env 放進映像檔docker run -p 8000:8000
host.docker.internal
| 問題 | 沒有容器時 | 容器裡的做法 |
|---|---|---|
| 套件版本 | 每台電腦各裝各的 | 映像檔建置時固定 |
| 中文字型 | macOS 有黑體,Linux 不一定有 | 映像檔裡直接安裝 |
| 資料存放 | 散落在啟動目錄 | 集中在 /app/data,可掛載 volume |
| 權限 | 常常以使用者本人或 root 執行 | 專用的非 root 使用者 |
| 是否正常運作 | 要自己打 /health |
Docker 的 HEALTHCHECK 自動檢查 |
LLM 本身不放進這個映像檔:Mistral 7B 的模型檔約 4.4GB,而且 Ollama 有官方映像檔。容器透過網路連到 Ollama,預設是主機上已經在跑的那一個。
docker build -t srs-review-agent .
python:3.12-slim
+ fonts-wqy-zenhei(Day 23 的 PDF 字型)
+ pip install -r requirements.txt
+ src/、tests/
+ 使用者 app(uid 1000)、/app/data
docker run(預設指令):uvicorn src.api_main:app → :8000
docker run ... python -m src.cli analyze /work/spec.md ← 同一個映像檔跑 CLI
容器 → http://host.docker.internal:11434 → 主機上的 Ollama
requirements.txt
srs-review-agent
Dockerfile、.dockerignore(新增)docker-compose.yml 以這個 Dockerfile 建置後端服務專案結構變化:
srs-review-agent/
├── Dockerfile ← 【新增】
├── .dockerignore ← 【新增】
├── requirements.txt
├── src/
└── tests/
需要 Docker(本文使用 Docker Desktop,Engine 29.7.2):
docker info --format '{{.ServerVersion}} {{.OperatingSystem}}'
# 29.7.2 Docker Desktop
在專案根目錄建立 Dockerfile:
# Day 25:SRS Review Agent 後端(FastAPI)與 CLI 共用的映像檔
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
# 中文 TrueType 字型:Day 23 的 PDF 報告需要嵌入(src/pdf_report.py 的 FONT_CANDIDATES)
RUN apt-get update \
&& apt-get install -y --no-install-recommends fonts-wqy-zenhei \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 先複製 requirements.txt:只改程式碼時,pip install 這一層可以沿用快取
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY src/ src/
COPY tests/ tests/
逐段說明:
python:3.12-slim:本系列在 macOS 上用的是 Python 3.14,但 slim 映像檔選擇更成熟的 3.12;requirements.txt 用的都是 >= 下限,兩個版本都能安裝PYTHONUNBUFFERED=1:print() 立即輸出,docker logs 才看得到即時訊息fonts-wqy-zenhei:文泉驛正黑,Day 23 的 FONT_CANDIDATES 裡已經列了它的路徑 /usr/share/fonts/truetype/wqy/wqy-zenhei.ttc。--no-install-recommends 與刪除 apt 清單,都是為了讓映像檔小一點requirements.txt:Docker 依層快取,這一層只在 requirements.txt 改變時才重建。改了程式碼重新建置,pip install 那一步直接沿用tests/ 也放進去:讓容器裡可以跑測試驗證映像檔;Day 22-24 的範例規格書也在 tests/fixtures/ 裡後半段:
# 以非 root 使用者執行;資料庫與緩存放在 /app/data,可掛載 volume 保留
RUN useradd --create-home --uid 1000 app \
&& mkdir -p /app/data \
&& chown app:app /app/data
USER app
ENV DATABASE_URL=sqlite:////app/data/srs.db \
SRS_CACHE_PATH=/app/data/cache/verify_cache.json \
OLLAMA_URL=http://host.docker.internal:11434 \
OLLAMA_MODEL=mistral
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/health', timeout=3)"
CMD ["uvicorn", "src.api_main:app", "--host", "0.0.0.0", "--port", "8000"]
app 只能寫 /app/data,程式碼目錄是唯讀的/app/data:Day 15 的 SQLite 資料庫、Day 12 的磁碟緩存。sqlite:////app/data/srs.db 有四條斜線:前三條是 URL 格式,第四條是絕對路徑的開頭OLLAMA_URL=http://host.docker.internal:11434:在容器裡,localhost 指的是容器自己。host.docker.internal 是 Docker Desktop 提供的主機名稱,指向主機本身HEALTHCHECK 用 Python 而不是 curl:slim 映像檔沒有安裝 curl,為了一個健康檢查多裝一個套件並不划算--host 0.0.0.0:uvicorn 預設只聽 127.0.0.1,那樣容器外連不進來SECRET_KEY 刻意不寫在 Dockerfile 裡。寫進映像檔的值,任何拿到映像檔的人都看得到,必須在執行時用 -e 傳入。
# Day 25:不放進映像檔的檔案
.venv/
venv/
.git/
.claude/
.pytest_cache/
**/__pycache__/
*.pyc
.DS_Store
.env
*.db
.cache/
frontend/node_modules/
frontend/dist/
reports/
docs/
三類檔案要排除:
.venv/、frontend/node_modules/。不排除的話,docker build 要先把它們全部傳給 Docker daemon.env(裡面可能有 SECRET_KEY)、*.db(開發用的資料庫,裡面有測試帳號)docs/、reports/
cd srs-review-agent
docker build -t srs-review-agent .
第一次建置約 46 秒,映像檔大小:
docker image ls srs-review-agent --format '{{.Repository}}:{{.Tag}} {{.Size}}'
# srs-review-agent:latest 414MB
docker run -d --name srs-api -p 8000:8000 -e SECRET_KEY=change-me srs-review-agent
curl http://localhost:8000/api/v1/health
{"status": "healthy", "version": "1.0.0", "timestamp": "2026-10-02T04:40:55.184042",
"components": {"detector": "ready", "llm": "mock", "cache": "ready", "jobs": 0}}
沒有設定 SRS_USE_OLLAMA=1,所以是 Day 13 的規則 + 模擬驗證模式。約 40 秒後,Docker 的健康檢查也通過了:
docker inspect --format '{{.State.Health.Status}}' srs-api
# healthy
docker exec srs-api whoami
# app
docker exec srs-api python -m pytest tests/ -q
111 passed in 4.37s
全部 111 個測試在 Python 3.12 的容器裡都通過,包含 Day 23 的 PDF 測試:容器裡有文泉驛字型,所以「字型必須嵌入」的測試沒有被略過。
同一個映像檔,換一個指令就是 CLI。把規格書所在的資料夾掛載到 /work:
docker run --rm -v "$PWD/tests/fixtures:/work" srs-review-agent \
python -m src.cli analyze /work/srs_small.md --format pdf -o /work/srs_small.report.pdf
PDF 報告已寫入 /work/srs_small.report.pdf
9 條需求,1 個衝突(高 1、中 0、低 0),待複核 1 個
產生的 PDF 約 34 KB。打開來,標題、長條圖標籤、待複核清單裡的「REQ-2.1.1 ↔ REQ-4.1.2」都正確顯示。主機上完全不需要安裝字型,字型在容器裡、也嵌入在 PDF 裡。
注意:容器以 uid 1000 執行。掛載的資料夾如果是其他使用者擁有、且沒有寫入權限,CLI 會無法寫出報告。macOS 的 Docker Desktop 會自動處理檔案權限;Linux 上可以用
--user "$(id -u):$(id -g)"以目前使用者的身分執行。
docker exec srs-api python -c "import urllib.request,json; \
print([m['name'] for m in json.load(urllib.request.urlopen('http://host.docker.internal:11434/api/tags'))['models']])"
# ['mistral:latest']
容器透過 host.docker.internal 看到了主機上的 Mistral。要讓 API 使用它,執行時加上 -e SRS_USE_OLLAMA=1 即可(Day 13、Day 20、Day 21 的設定都會生效)。
requirements.txt 混合了執行與測試的依賴:pytest 也被裝進了正式映像檔。好處是可以在容器裡驗證,代價是多幾 MB。規模變大後,可以拆成 requirements.txt 與 requirements-dev.txt
>= 下限代表半年後重建,可能裝到不同的版本。正式發佈時應該用 pip freeze 或 pip-tools 產生鎖定檔host.docker.internal:Docker Desktop(macOS、Windows)內建這個名稱;Linux 需要在 docker run 加上 --add-host=host.docker.internal:host-gateway。Day 26 的 Compose 檔會處理git add Dockerfile .dockerignore
git commit -m "Day 25: Docker 映像檔(中文字型、非 root、健康檢查、API 與 CLI 共用)"
現在後端是一個容器,但前端還得另外 npm run dev,Ollama 也要另外啟動,SECRET_KEY、volume、連接埠都要記得手動帶參數。明天 Day 26 我們用 Docker Compose 把後端、前端(nginx)與可選的 Ollama 寫進同一個設定檔,docker compose up 一次啟動,並驗證重啟後資料仍在。