from Pixabayfrom Pixabay

📌 這是 WeaMind 系列 的第 3 篇。
本系列以真實世界專案為背景,記錄重要技術實作與經驗分享。

uv 是近年來快速竄起的 Python 套件管理器,憑藉其最關鍵的核心特性——快😎,在 Python 開發圈吸引了不少關注與採用。

之前我已寫過〈Python 套件管理器 uv 介紹——與 Poetry 比較〉介紹。有興趣的讀者歡迎參考。

本文是 uv 系列的第二篇,聚焦於 Dockerfile

當我要將「使用 uv 的 Python 專案」容器化時,我發現相關的討論與介紹較少,所以花了一些時間摸索。

本文目標是大幅減少你的試錯時間,並提出我的實踐與看法。

系列:Python 後端專案容器化

系列:Python 套件管理——使用 uv


從 Poetry 到 uv:Dockerfile 寫法大不同

不久前,我才寫下了〈Docker 教學:用 Multi-stage build 建立 Poetry 虛擬環境〉,講述如何在 Docker 中,使用 Poetry 建立容器內的 Python 執行環境。

本文算是該篇文章的「uv 姐妹作」,但是,做法非常不同!

我本以為,只要簡單模仿 Poetry 的 multi-stage build 方式就好,但實際操作後才發現問題多多。

畢竟兩者的指令不同,尤其對於「全域安裝套件」有著截然不同的做法。

這裡就不仔細比較差異了,經過一番探索,我們已經得到了最佳解😇

那就是使用 uv 團隊推出的官方 Docker image!


使用 uv 官方 image

關於 uv 容器化議題,uv 開發團隊直接推出了專用的官方 image

該 image 已內建 uv,且在體積上進行了最佳化,大概只有 60 MB。(指的是使用 slim 版本的 Linux 基底)

透過它,你可以省去繁瑣的安裝設定,並且在安全性上更有保障。

如果習慣從 Docker hub 拉取,則可參考這裡

一般而言,仍建議你使用 GitHub 托管的版本——也就是ghcr.io/開頭的版本。因為它才是「第一手」。

例如:

1
ghcr.io/astral-sh/uv:python3.12-bookworm-slim

建議用它作為 Dockerfile 的基底 image。

如何使用官方 image 撰寫 Dockerfile

有了 image,那我們要怎麼撰寫 Dockerfile 呢?

對此,我們也不需要自己苦苦思考了,大部分挑戰都已經有了答案——請直接參考這個官方倉庫中的 Dockerfile 寫法!

強烈建議你,如果沒有特殊需求,就 follow 它的寫法,因為 uv 的操作有很多繁瑣的細節,考驗著你對該工具的了解,尤其在 Docker 容器化的場景更是如此。

官方的 Dockerfile 也非常細心,在多個指令都加上了簡要的註解,讓你知其然又知其所以然。

其實看過其中一些指令內容就知道,要自己寫肯定很難寫得這麼好。

所以我們還是盡可能參考甚至遵照這份 Dockerfile 的寫法。

可以少走很多彎路。


為什麼不需要 Multi-stage build?

前述〈Docker 教學:用 Multi-stage build 建立 Poetry 虛擬環境〉教你如何用 multi-stage build 建立 Poetry 虛擬環境。

因為 Poetry 和 uv 這類工具有各自需要的執行環境與依賴套件,不那麼適合在 Docker 建構過程中直接安裝並封裝成 image,這會造成一定程度的空間浪費與冗餘

所以我們選擇用 multi-stage build 來建立 image。省去不必要的依賴,有效減少最終 image 的體積。

那為什麼這裡就沒有這麼做呢?

原因很簡單,官方已經提供這份簡單、乾淨的 image,讓你直接開箱即用。雖然最終的 image 中仍會有 uv,但它的體積佔用已然經過最佳化。

此時自己再搞個 multi-stage build,也省不了多少空間,還徒增 Dockerfile 的複雜度與出錯可能。

MCP 時代需要 uv

附帶一提,在這個 MCP(模型上下文協定,Model Context Protocol)盛行的時代,我們可以看到 MCP Server 的開發,主要由兩種語言佔多數:

  1. TypeScript(JavaScript)
  2. Python

如果你想用 Python 來開發 MCP 服務,並希望讓使用者執行uv run <你定義的指令>來啟動 MCP Server(這是目前主流做法),那 uv 就需要存在你的最終 image 中。

延伸閱讀:使用 uv 輔助開發 MCP 伺服器並安裝到 Claude Desktop 與 VS Code

如此一來,自然也不需要透過 multi-stage build 來移除 uv 了。

我就是要 Multi-stage build!

不同專案有不同需求,如果你真的需要 multi-stage build 以進一步節省空間的話,同一個倉庫中有另一個 Dockerfile 可供參考。


uv 與 pip / requirements.txt 混搭法

其實,uv 對 pip 的相容性相當不錯——畢竟它有子命令uv pip

因此,你也可以不用官方的 uv image,而是使用一般 Python image,再直接搭配requirements.txt安裝容器中的 Python 虛擬環境就好。

好處是延續了既有的 pip 開發習慣,降低認知負擔。

重點是,官方還非常貼心地準備了 pre-commit hook,來協助你輕鬆同步uv.lockrequirements.txt

1
2
3
4
5
6
repos:
- repo: <https://github.com/astral-sh/uv-pre-commit>
# uv version.
rev: 0.8.0
hooks:
- id: uv-export

如此一來,就不會再發生前文中提到的「在更新poetry.lock時,常常會忘記同步到requirements.txt」這類問題。

相關文章:Python 開發:pre-commit 設定 Git Hooks 教學

此外,你也可以使用官方 image 但保留requirements.txt作為安裝依據,Dockerfile 寫法同樣簡單。

以上這兩種混搭方式的具體實作,可以參考 JumpingCode 資料科學手記的這篇〈如何使用 Python 套件管理工具「uv」取代 pip 來加速 Docker Image 的建立〉。

其中的「實測檔案」一節就提供了完整的 Dockerfile 範例。


我的 Dockerfile

最後附上 WeaMind 專案中,我目前使用的 Dockerfile

我對官方寫法做了一定的刪減,主要是移除專案作為第三方套件的相關設定,並簡化 Dockerfile 的快取策略。

如果你沒有把握,建議和 AI 討論或者直接 follow 官方的寫法就好。

此外,我還加上了一些註解,供讀者參考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

# Use 'code' to avoid confusion with the 'app' directory
WORKDIR /code

# Compile bytecode and avoid symlinks
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

# ---------- layer 1: heavy dependencies ----------
# Cache only invalidates when these two files change
COPY pyproject.toml uv.lock ./

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev

# ---------- layer 2: application code ----------
COPY . /code

# Put the venv at the beginning of PATH
ENV PATH="/code/.venv/bin:$PATH"

# Do not use uv as the entrypoint
ENTRYPOINT []