iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0

Day 24 完成了命令列工具,從一份 Markdown 規格書直接產生報告。但要在另一台電腦上執行,清單已經不短了:

  • Python 3.12 以上,以及 requirements.txt 的十幾個套件
  • 中文 TrueType 字型(Day 23:沒有字型就無法產生 PDF)
  • 環境變數:DATABASE_URL、SECRET_KEY、OLLAMA_URL……
  • 還要記得 test.db 會建在啟動目錄(Day 15)

今天把這些全部打包進一個 Docker 映像檔。同一個映像檔可以啟動 Web API,也可以直接執行 CLI。

這一天在系列中的位置

第 4 週:發佈與開源

  • Day 22-23 → 審查報告(Markdown、PDF)
  • Day 24 → 命令列工具
  • Day 25 → Docker 容器化 ← 今天
  • Day 26 → Docker Compose:一次啟動後端、前端與 Ollama

今日目標

讀完這篇後,你會完成:

  1. Dockerfile:Python 3.12、中文字型、非 root 使用者、健康檢查
  2. .dockerignore:不把虛擬環境、資料庫、.env 放進映像檔
  3. 在容器中執行 API:docker run -p 8000:8000
  4. 在容器中執行 CLI:掛載規格書資料夾,產生 PDF
  5. 連到主機上的 Ollama:host.docker.internal
  6. 在容器裡跑完整測試

問題背景:容器化要解決什麼

問題 沒有容器時 容器裡的做法
套件版本 每台電腦各裝各的 映像檔建置時固定
中文字型 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(新增)
  • 下游:Day 26 的 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

代碼示例

1. Dockerfile

在專案根目錄建立 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"]
  • 非 root 使用者:容器裡的程式被攻破時,攻擊者拿到的不是 root。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 傳入。

2. .dockerignore

# 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

執行 API

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

同一個映像檔,換一個指令就是 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)" 以目前使用者的身分執行。

連到主機上的 Ollama

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 的設定都會生效)。


權衡與限制

  • 映像檔 414MB:Python 套件佔 125MB,文泉驛字型佔 17MB,其餘主要是 Python 3.12 基礎映像檔。可以用多階段建置進一步縮小,但收益有限
  • requirements.txt 混合了執行與測試的依賴:pytest 也被裝進了正式映像檔。好處是可以在容器裡驗證,代價是多幾 MB。規模變大後,可以拆成 requirements.txt 與 requirements-dev.txt
  • 版本沒有鎖定:>= 下限代表半年後重建,可能裝到不同的版本。正式發佈時應該用 pip freeze 或 pip-tools 產生鎖定檔
  • Linux 上的 host.docker.internal:Docker Desktop(macOS、Windows)內建這個名稱;Linux 需要在 docker run 加上 --add-host=host.docker.internal:host-gateway。Day 26 的 Compose 檔會處理
  • Day 17 的會話在記憶體裡:容器重啟後所有人都要重新登入。這在容器環境更常發生(更新映像檔就會重啟)

提交變更

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 一次啟動,並驗證重啟後資料仍在。


上一篇
Day 24:命令列工具
系列文
解決需求規格書矛盾:用 Claude Code × MCP 實作自律型文檔審查 Agent 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言