Python 專案要跑上 VPS 從來都不是「pip install -r requirements.txt」那麼簡單。Ubuntu 22 到 Debian 13 之間,system Python 版本會跳、C extension 得看 wheel 是否跟 glibc 相容、開發者本機藏著 pyenv shim 跟一堆 virtualenv,push 到伺服器又得從頭重建一次。這一整串 friction 在 2025 年還是要靠 pyenv 加 poetry 加 pip-tools 三個工具接力應付。uv 這支 Astral 出品的 Rust 工具在 2026 年 9 月 15 日推進到 0.12.15,把上面每一段接口一起吞下去——pyproject.toml 由它讀、Python 直譯器由它下載、lockfile 由它 resolve、Docker 映像檔的建置流程由它主導。OpenAI 在 3 月完成對 Astral 的收購,路線圖並沒有轉彎,9 月連續補上 code signing 跟 exclude-newer 這類供應鏈安全需求。要在 VPS 上跑 Python 服務,這半年開始 uv 是預設答案。
uv.lock 跟舊時代 requirements.txt 不是同一種東西
pip freeze 出來的 requirements.txt 是「這臺機器上 pip list 的 snapshot」,換平臺換 Python 版本就會歪掉。Windows 開發者跟 Linux runner 各自產一份、CI 上跑的環境跟 production 又是第三份,這是 Python 專案「在我機器上跑得起來」老症狀的源頭。
uv 從 0.5 之後生成的 uv.lock 是 universal resolution 的結果,同一個檔案裡帶著環境 markers,跨作業系統、跨架構、跨 Python 版本都可以決定當前平臺該裝哪個版本。開發者的 MacBook Arm64 跟臺灣機房的 x86 VPS 用同一份 uv.lock,uv sync --locked 出來的環境從 wheel 到 metadata 完全一致。
實務上這個特性最大的差別在 Docker build。uv sync 在 build stage 靠 lockfile 決定要下載哪些 wheel,同一份 lockfile 在開發者本機、CI runner、正式機都會走同一條 resolution。reproducible build 這件事不必再靠 pip-compile 加 hash pin 那種手工做法。
pyenv、venv、pip 這三塊被同時抽掉
老 Python 開發者的工作環境長這樣:pyenv 管 Python 版本、venv 或 virtualenv 開隔離環境、pip 裝套件、pip-tools 或 poetry 產 lock、有時候再加一支 pipx 跑獨立工具。五個工具彼此的邊界從來沒被清楚定義,缺哪一個都會卡住。
uv 把這五塊全部收進同一支 binary。uv python install 3.13 把 CPython 3.13 拉下來放進 ~/.local/share/uv/python,不動 system Python 也不改 PATH。uv venv 建隔離環境、uv add fastapi 加套件並同時更新 pyproject.toml 跟 uv.lock,uv run 直接在該環境裡執行指令。全部走 Rust 實作,冷 cache 安裝時間也在 300ms 到 1 秒級。
比較有爭議的一點是「該不該讓 uv 管 Python 直譯器」。傳統派主張伺服器上要用 apt 或 dnf 裝系統 Python,好處是 security update 走 distro。這個想法在 VPS 部署場景反而是負擔——系統 Python 版本被鎖在 3.11 之類的舊版,專案要用 3.13 的新語法就得整套環境 workaround。務實的做法是伺服器留一份 system Python 給 OS 工具用,應用程式跑 uv 管的獨立 Python,跟 Node.js 靠 nvm 或 fnm 管版本的模型完全一致。
Dockerfile 要拆兩階段才不會把 uv 的速度優勢吃掉
單階段 Dockerfile 把 uv sync 跟應用程式碼塞在同一層,每次 git push 都會讓所有 dependency layer 重下載,這是新手最常見的錯法。
正確的寫法是兩階段:前段 build stage 用官方 ghcr.io/astral-sh/uv image 加 --mount=type=cache 把 uv 的 cache 綁到 BuildKit cache mount,執行 uv sync --locked --no-install-project --no-dev 只裝依賴,再單獨 COPY 應用程式碼進去跑 uv sync --locked --no-dev 補上專案本身。後段 runtime stage 從 build stage COPY /app/.venv 出來到 distroless 或 slim base image,把 uv binary 本身留在 build stage、不帶進 production image。
幾個容易踩雷的細節:UV_LINK_MODE=copy 在 BuildKit cache 掛載時是必要的,否則 uv 預設用 hardlink 會跨 filesystem 失敗。UV_COMPILE_BYTECODE=1 讓 build 時就把 .pyc 生出來,冷啟動延遲能砍 20% 到 50%。UV_PROJECT_ENVIRONMENT=/opt/venv 把 venv 位置從 .venv 搬到絕對路徑,避免多階段 COPY 時路徑衝突。這三個環境變數是 uv 官方 Docker guide 上明列的 production 建議,值得寫死進 Dockerfile 而不是靠 CI runner 傳。
多階段 build 的另一個延伸是 base image 的選擇。過去 Python 專案常用 python:3.13-slim 或 python:3.13-alpine,前者體積偏大、後者 musl libc 常常撞到 C extension 沒有 musl wheel 的問題。改走 uv 主導的部署後,runtime stage 可以直接用 debian:trixie-slim 或 gcr.io/distroless/python3-debian12,因為 Python 直譯器已經由 uv 塞進 venv、不再依賴 base image 內建的 python 執行檔,image 大小落在 80MB 到 120MB 之間,比舊模式的 250MB 到 400MB 有明顯差距。
0.12 補上的 code signing 跟 exclude-newer 是供應鏈的兩塊補丁
xz utils 事件跟 SolarWinds 那些教訓走了兩年,Python 生態直到 uv 0.12 才把幾個具體 supply chain 洞補起來。
0.12.9 開始 macOS 跟 Windows 的 release archive 都是 code signed 的。macOS 走 Apple Developer ID 證書加 Apple 公證,Windows 走 Azure Artifact Signing 加 timestamped Authenticode。CI runner 從 GitHub Releases 抓 uv binary 時,可以用 codesign 或 signtool 驗簽,避免中間人塞改過的 binary。這對於在臺灣 VPS 上跑 GitHub Actions self-hosted runner 的團隊尤其實用,跨海抓 binary 的完整性有原生簽章可以驗。
0.12.10 把 --exclude-newer 的行為擴大到 lockfile。這個 flag 原本只影響 resolve 階段,讓 uv 忽略某個日期之後才上 PyPI 的版本。0.12.10 之後 lockfile 生成時會把 exclude-newer 之後上架的 distribution 直接排除,等於在 lockfile 層固定住某個時間點的 PyPI 快照。針對「新版本被投毒」這類供應鏈攻擊,這是 Python 生態第一次有官方 lockfile 級的時間點鎖定,比自己在 CI 拉 mirror 快照維護成本低一個數量級。
OpenAI 收購後的替代方案還撐得住嗎
3 月的收購案在社群裡掀了兩波質疑。第一波是「單一 vendor 掌握 pip、ruff、ty 三個入口是不是太危險」,第二波是 OpenAI 財力雄厚但過去對 open source 沒有長期承諾的記錄。
實際觀察下來,過去半年 uv 的路線圖沒有明顯轉向 OpenAI 自家產品,release cadence 反而變快,9 月兩週內連續出 0.12.9 到 0.12.15 這種節奏在收購前不常見。dual license MIT/Apache 2.0 也沒有動。真正擔心 vendor 集中風險的專案,還是可以繼續走 Poetry 或 Hatch,但這兩個工具跟 uv 之間的效能差距是 10 倍到 100 倍等級,換回去的成本不只在 CI 時間,還在開發者本機的迭代速度。
比較保守的做法是 pyproject.toml 保持標準 PEP 621 格式,uv.lock 版控入 repo,這樣就算某天 uv 出事,用 pip-tools 或 Poetry 從 pyproject.toml 重新 resolve 也只是幾小時的遷移成本。lock-in 的實際 blast radius 沒有想像中大,這也是目前建議直接切過去的主要原因。
一句 uv sync 換掉的 legacy 部署腳本
用一份 uv 收乾之後的 Dockerfile 重寫 Python 專案的部署腳本,等於把 pyenv 版本切換、venv 啟動、pip install、pip-tools 生 lock 這四段流程壓到 uv sync --locked 一句。CI 上少了一半的維運腳本、VPS 上少一份系統 Python 相依、image 大小平均縮 30% 到 40%,冷啟動時間也明顯下降。
NCSE Network 的 VPS 主機跑在臺灣是方電訊機房,Intel Gold CPU 加 NVMe SSD 的組合特別適合這種靠 uv sync 快速拉起 Python 服務的部署模型——冷 build 到 hot start 之間的 I/O 瓶頸大幅收斂。有規劃把 Python 應用從舊的 pyenv + virtualenv 模式遷到 uv 主導的容器化部署,可以到 ncse.tw 看看 VPS 主機規格與線路配置。