本篇聚焦在「映像瘦身與強化安全」。依舊延續前面章節的專案骨架、依賴鎖定、結構化日誌與 API 分層,但不談 CI/CD(之後再提)。你可以直接把範例放進專案,立刻得到乾淨、可重現、預設安全的容器。
uv.lock
)在建置時強制一致依賴,避免「今天裝到不一樣的包」那種開發者崩潰。以 Day 4 的建議為例,容器只帶執行所需的內容:
my_project/
├─ pyproject.toml
├─ uv.lock # 鎖住依賴
├─ src/my_project/ # 執行用程式碼
├─ tests/ # 只在本機或開發流程跑,不進 runtime
└─ .dockerignore
.dockerignore
(先放這份,之後你再微調):
.git
__pycache__/
*.pyc
.env*
tests/
notebooks/
dist/
build/
.hatch/
.cache/
.vscode/
.idea/
重點:builder 安裝依賴與可選的原生套件,runtime 只拷貝「結果」。同時建立非 root 使用者,並以 stdout 打 JSON 日誌(對 Day 13 友善)。
# syntax=docker/dockerfile:1.7
############################
# Stage 1: builder(有編譯工具)
############################
FROM python:3.12-slim AS builder
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
UV_SYSTEM_PYTHON=1
# 依實際需求加入原生依賴(例如需要編譯的 wheels)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential curl ca-certificates && \
rm -rf /var/lib/apt/lists/*
# 安裝 uv(高速、可重現)
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"
WORKDIR /app
# 先複製 metadata 與鎖檔以最大化快取命中
COPY pyproject.toml uv.lock ./
# 依鎖檔同步依賴到虛擬環境(不含 dev 依賴)
RUN uv sync --frozen --no-dev --python=/usr/local/bin/python
# 再帶入程式碼(避免動到程式碼就讓前面步驟失效)
COPY src/ ./src/
############################
# Stage 2: runtime(最小執行環境)
############################
FROM python:3.12-slim AS runtime
# 建立非 root 使用者(固定 UID/GID 便於掛載)
RUN groupadd -g 10001 app && \
useradd -m -u 10001 -g 10001 -s /usr/sbin/nologin app
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
RUN mkdir -p /app && chown -R app:app /app
# 從 builder 帶入程式碼與已解決好的虛擬環境
COPY --from=builder /app/src/ /app/src/
COPY --from=builder /root/.cache/uv/venv/*/ /opt/venv/
ENV PATH="/opt/venv/bin:${PATH}"
# 服務埠
EXPOSE 8000
# 切換非 root
USER app:app
# 建議在執行時加入 --read-only 並掛載 /tmp 或資料目錄
# 例如:docker run --read-only -v tmpfs:/tmp ...
# 以 Uvicorn 起服務(依你的專案模組路徑調整)
CMD ["uvicorn", "my_project.adapters.web.app:app", "--host", "0.0.0.0", "--port", "8000"]
app:app
,固定 UID/GID。USER app:app
。-read-only
,需要寫入就掛 tmpfs 或 volume。NET_ADMIN
, SYS_PTRACE
)。uv.lock
),並加上 -frozen
,確保安裝版本完全一致。本地測試:
# 建置
docker build -t myapp:dev .
# 執行(唯讀檔系統 + 暫存寫入)
docker run --rm -p 8000:8000 \
--read-only \
--tmpfs /tmp:rw,size=64m \
myapp:dev
健康檢查與日誌建議:
/healthz
或 /readyz
簡單回應與關鍵依賴檢查。症狀 | 可能原因 | 解法 |
---|---|---|
執行期找不到原生動態庫 | builder 有裝套件,runtime 沒有 | 把必要的系統動態庫也安裝在 runtime,或改至 builder 輸出完全靜態/對應 wheel |
非 root 下啟動失敗 | 檔案權限或目錄不存在 | 在 Dockerfile 中 chown 必要目錄,或改用 volume 並指定擁有者 |
每次改一行就重裝依賴 | 複製順序不對 | 先 COPY pyproject.toml/uv.lock ,完成安裝後才 COPY src/ |
需要可寫路徑 | rootfs 唯讀 | 在 run 時掛載 --tmpfs /tmp 或將 data/ 做成命名 volume |
體積仍然過大 | apt 快取未清、帶入多餘檔 | rm -rf /var/lib/apt/lists/* 、調整 .dockerignore 、檢查是否把 tests/notebooks 帶進 runtime |
slim
或你確實需要的最小發行版。apt-get install
後清理快取與 headers。為了對齊容器啟動命令,入口建議穩定且清楚,例如 my_project.adapters.web.app:app
,裡面維持乾淨的組態與日誌初始化:
# src/my_project/adapters/web/app.py
from fastapi import FastAPI
def create_app() -> FastAPI:
app = FastAPI(title="My Service")
# init logging to JSON here (structlog / logging.config)
@app.get("/healthz")
def healthz():
return {"status": "ok"}
return app
app = create_app()
容器化做對了,等於把「可重現」與「預設安全」內建到產品生命週期。多階段讓映像乾淨,非 root 把風險打到最低;鎖檔與穩定入口讓你今天建什麼,明天還是那個。等後續要談流程與自動化,再把這套基礎往外延伸就好。