iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Software Development

30 天 Python 專案工坊:環境、結構、測試到部署全打通系列 第 24

Day 24 - 開發體驗:Dev Container / VS Code 與 Hatch scripts

  • 分享至 

  • xImage
  •  

目標:把「專案能跑」升級成「任何人打開就能順順開始寫」。用 Dev Container 鎖住開發環境、VS Code 當入口、Hatch scripts 把雜事一鍵化。


為什麼要 Dev Container

你不是真的想在每台機器都重建同一堆工具鍋吧?Dev Container 讓你把開發環境塞進容器:套件版本、系統依賴、工具鏈,全寫在定義檔。新人 clone 下來,按「Reopen in Container」,環境就緒。

這件事跟我們一路鋪的「src/ + tests/」目錄骨架與可安裝套件設計是同一脈絡:一致、可重現、清楚分工。把核心程式放 src/,測試放 tests/,避免本機環境魔法污染匯入路徑,才有真正穩定的開發體驗。

而在容器層,把建置依賴留在 builder,runtime 只裝最小執行體與程式碼,搭配鎖檔保證依賴一致,這些你在 Day 23 都看過了。


專案骨架(加上 .devcontainer)

沿用既有專案結構,新增 Dev Container 設定目錄:

my_project/
├─ .devcontainer/
│  ├─ devcontainer.json
│  ├─ Dockerfile           # 可選:若你想沿用多階段 Dockerfile 思路
│  └─ postCreate.sh        # 可選:容器建好後的一次性初始化
├─ pyproject.toml
├─ uv.lock                 # 鎖住依賴(別裝到不一樣的包)
├─ src/my_project/
└─ tests/

鎖檔進版控,能讓每個人裝到的套件與你的 CI/docker 建置一致。


devcontainer.json(最小可用)

{
  "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,讓容器建好後自動把本地工作流就位。


Dockerfile(開發版,沿用多階段與非 root 概念)

# 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 保持一致。


Hatch scripts:把雜事變成一行字

我們在 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"

好處不重複講了:一致入口、少背指令、更新流程只改一處。


VS Code 整合:指令面板到偵錯全部串上

1) 任務(tasks.json)

把常用 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 }
  ]
}

2) 偵錯(launch.json)

提供 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
    }
  ]
}


一次到位的「打開就能寫」流程

  1. Clone 專案,Reopen in Container

    容器依 devcontainer.json 建好,使用者身份已切到固定 UID/GID 的非 root。

  2. 自動 postCreate

    Hatch 會執行 dev:bootstrap,檢查工具是否就緒。

  3. 跑健康檢查

    hatch run dev:check 一鍵執行 ruff、black、mypy、pytest。

  4. 啟動服務

    hatch run dev:serve 用 uvicorn 起來,符合我們在 API/容器篇建議的穩定入口。

  5. 打開 REST Client / 前端對接測通路

    服務健康端點可回 "ok",這在容器化最佳實務也示範過。


團隊協作的小配方

  • .vscode/.devcontainer/ 一起版控:新同事不用問「要裝什麼」。
  • uv.lock 必須在 PR 裡審:依賴變動需要被看見,這是可重現的基石。
  • 維持 src/ + tests/ 的穩定分工:避免因為 Dev Container 很方便就開始把東西丟到根目錄跑魔法匯入。

Debug 食譜

症狀 可能原因 快速解法
「換一行就重裝半小時」 Docker 層快取失效,複製順序不對 先 COPY pyproject.toml/uv.lock,安裝完再 COPY src/
容器內權限錯誤 以非 root 跑,路徑沒權限 在 Dockerfile/compose 事先 chown 目錄或掛載 volume 時指定擁有者。
容器過大或建置太慢 apt 快取沒清、帶了不該帶的檔 apt 快取、調整 .dockerignore,只把執行需要的檔案帶進 runtime。
本機與 CI 行為不一致 沒鎖依賴或大家各裝各的 使用 uv.lock--frozen 同步依賴,CI 與本機一致。

延伸:把 Nox 接進來

多 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 負責「把入口變得一樣」。這三件事合起來,就是開發體驗工程化。

別把「能跑起來」當結束,那只是開始。結束是「任何人、在任何地方、今天或下週,一打開就能寫,而且結果一樣」。這正是我們從目錄結構、到鎖檔、到多階段容器一路推的核心。


上一篇
Day 23 -容器化最佳實務:多階段 Dockerfile 與非 root 執行
系列文
30 天 Python 專案工坊:環境、結構、測試到部署全打通24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言