目標:把「專案能跑」升級成「任何人打開就能順順開始寫」。用 Dev Container 鎖住開發環境、VS Code 當入口、Hatch scripts 把雜事一鍵化。
你不是真的想在每台機器都重建同一堆工具鍋吧?Dev Container 讓你把開發環境塞進容器:套件版本、系統依賴、工具鏈,全寫在定義檔。新人 clone 下來,按「Reopen in Container」,環境就緒。
這件事跟我們一路鋪的「src/ + tests/」目錄骨架與可安裝套件設計是同一脈絡:一致、可重現、清楚分工。把核心程式放 src/
,測試放 tests/
,避免本機環境魔法污染匯入路徑,才有真正穩定的開發體驗。
而在容器層,把建置依賴留在 builder,runtime 只裝最小執行體與程式碼,搭配鎖檔保證依賴一致,這些你在 Day 23 都看過了。
沿用既有專案結構,新增 Dev Container 設定目錄:
my_project/
├─ .devcontainer/
│ ├─ devcontainer.json
│ ├─ Dockerfile # 可選:若你想沿用多階段 Dockerfile 思路
│ └─ postCreate.sh # 可選:容器建好後的一次性初始化
├─ pyproject.toml
├─ uv.lock # 鎖住依賴(別裝到不一樣的包)
├─ src/my_project/
└─ tests/
鎖檔進版控,能讓每個人裝到的套件與你的 CI/docker 建置一致。
{
"name": "my_project dev",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"features": {
// 需要額外 CLI 就在這裡宣告(可留空)
},
"remoteUser": "app",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"ms-toolsai.jupyter"
],
"settings": {
"python.defaultInterpreterPath": "/opt/venv/bin/python",
"python.testing.pytestEnabled": true
}
}
},
"postCreateCommand": "hatch run dev:bootstrap"
}
這裡的 postCreateCommand
會咬上 Hatch scripts,讓容器建好後自動把本地工作流就位。
# 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
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 /workspace
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --python=/usr/local/bin/python
COPY src/ ./src/
# Stage 2: dev runtime
FROM python:3.12-slim AS dev
RUN groupadd -g 10001 app && useradd -m -u 10001 -g 10001 -s /usr/sbin/nologin app
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /workspace
COPY --from=builder /workspace/src/ /workspace/src/
COPY --from=builder /root/.cache/uv/venv/*/ /opt/venv/
ENV PATH="/opt/venv/bin:${PATH}"
USER app:app
重點依然是:依賴在 builder 解決,runtime 拿結果;固定非 root 使用者;用鎖檔 --frozen
保持一致。
我們在 Day 8 建議把常用工作流寫進 pyproject.toml
的 Hatch scripts,開發者只要記一個入口就能跑測試、lint、typecheck。
範例設定(節錄 pyproject.toml
)
[project.optional-dependencies]
dev = ["pytest", "pytest-cov", "ruff", "black", "mypy", "httpx", "fastapi"]
[tool.hatch.envs.dev]
features = ["dev"]
[tool.hatch.envs.dev.scripts]
bootstrap = [
"ruff --version",
"python -c \"print('venv ready')\""
]
check = [
"ruff check .",
"black --check .",
"mypy src/",
"pytest -q"
]
test = "pytest -q"
fmt = "black ."
type = "mypy src/"
serve = "uvicorn my_project.adapters.web.app:app --reload --port 8000"
好處不重複講了:一致入口、少背指令、更新流程只改一處。
把常用 Hatch 指令變成 VS Code 任務,方便用快捷鍵或命令面板觸發:
{
"version": "2.0.0",
"tasks": [
{ "label": "check", "type": "shell", "command": "hatch run dev:check", "group": "build" },
{ "label": "test", "type": "shell", "command": "hatch run dev:test", "group": "test" },
{ "label": "fmt", "type": "shell", "command": "hatch run dev:fmt" },
{ "label": "serve","type": "shell", "command": "hatch run dev:serve", "isBackground": true }
]
}
提供 API 服務的即時偵錯:
{
"version": "0.2.0",
"configurations": [
{
"name": "API (uvicorn)",
"type": "python",
"request": "launch",
"module": "uvicorn",
"args": ["my_project.adapters.web.app:app", "--reload", "--port", "8000"],
"python": "/opt/venv/bin/python",
"justMyCode": true
}
]
}
Clone 專案,Reopen in Container
容器依 devcontainer.json
建好,使用者身份已切到固定 UID/GID 的非 root。
自動 postCreate
Hatch 會執行 dev:bootstrap
,檢查工具是否就緒。
跑健康檢查
hatch run dev:check
一鍵執行 ruff、black、mypy、pytest。
啟動服務
hatch run dev:serve
用 uvicorn 起來,符合我們在 API/容器篇建議的穩定入口。
打開 REST Client / 前端對接測通路
服務健康端點可回 "ok"
,這在容器化最佳實務也示範過。
.vscode/
與 .devcontainer/
一起版控:新同事不用問「要裝什麼」。uv.lock
必須在 PR 裡審:依賴變動需要被看見,這是可重現的基石。src/ + tests/
的穩定分工:避免因為 Dev Container 很方便就開始把東西丟到根目錄跑魔法匯入。症狀 | 可能原因 | 快速解法 |
---|---|---|
「換一行就重裝半小時」 | Docker 層快取失效,複製順序不對 | 先 COPY pyproject.toml /uv.lock ,安裝完再 COPY src/ 。 |
容器內權限錯誤 | 以非 root 跑,路徑沒權限 | 在 Dockerfile/compose 事先 chown 目錄或掛載 volume 時指定擁有者。 |
容器過大或建置太慢 | apt 快取沒清、帶了不該帶的檔 | 清 apt 快取、調整 .dockerignore ,只把執行需要的檔案帶進 runtime。 |
本機與 CI 行為不一致 | 沒鎖依賴或大家各裝各的 | 使用 uv.lock 並 --frozen 同步依賴,CI 與本機一致。 |
多 Python 版本驗證或多矩陣測試,用 Nox 補一腳:
# noxfile.py
import nox
@nox.session(python=["3.10","3.11","3.12"])
def tests(session):
session.install(".[dev]")
session.run("pytest","-q")
開發者在容器裡直接 nox
,就能跑跨版本測試矩陣。
Dev Container 負責「把每個人的機器變得一樣」,Hatch scripts 負責「把每個人的操作變得一樣」,VS Code 負責「把入口變得一樣」。這三件事合起來,就是開發體驗工程化。
別把「能跑起來」當結束,那只是開始。結束是「任何人、在任何地方、今天或下週,一打開就能寫,而且結果一樣」。這正是我們從目錄結構、到鎖檔、到多階段容器一路推的核心。