はじめに
uvで本番環境のdockerイメージをビルドするにはどのような方法が良いのか調査したいと思い、今回はFastAPIとMySQLを使ったサンプルアプリケーションを題材に調査してみました。
MySQLドライバには
mysqlclient
を使います。このドライバはネイティブライブラリに依存していて、例えばDebianではビルド時に
pkg-config
gcc
、実行時には
default-libmysqlclient-dev
が必要です。
このような場合、マルチステージビルドを使ったほうが、最終のイメージサイズを小さくすることができるので、今回はそれを実践してみたいと思います。
サンプルプロジェクトの作成
まずはuvを使ったサンプルプロジェクトを作成します。
pyproject.toml
では、fastapiとmysqlclientに依存しています。dev dependancyとしてpytestに依存しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | [project] name = "uv-docker" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ "fastapi[standard]>=0.115.12", "mysqlclient>=2.2.7", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [dependency-groups] dev = [ "pytest>=8.3.5", ] |
src/uv_docker/app.py
を作成します。こちらはFastAPIのアプリケーションで、mysqlclientを使ってSELECTするサンプルとなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import MySQLdb from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): db = MySQLdb.connect( host="db", user="root", password="password", database="information_schema" ) cursor = db.cursor() cursor.execute("SELECT * FROM TABLES") result = cursor.fetchall() cursor.close() db.close() print(result) return {"HelloWorld": result} |
シングルステージビルド
まずは、シングルステージのDockerfileを試してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | FROM python:3.13-slim # UVのインストール COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # アプリケーションのソースコードをコピー COPY . /app WORKDIR /app # mysqlclientのインストールに必要なパッケージをインストール RUN apt update && apt install -y pkg-config default-libmysqlclient-dev gcc # バイトコードのコンパイルを有効 (起動時間が短縮される傾向がある) ENV UV_COMPILE_BYTECODE=1 # 依存パッケージのインストール RUN uv sync --frozen --no-cache --no-group dev # FastAPIを起動 CMD ["/app/.venv/bin/fastapi", "run", "/app/src/uv_docker/app.py", "--port", "80", "--host", "0.0.0.0"] |
このイメージのポイント
- プロダクション向けビルドで推奨されている
ENV UV_COMPILE_BYTECODE=1
を指定しています。この環境変数を指定することで、ビルド時にバイトコード (.pycファイル) が生成されるため、起動時間が短縮される傾向にあるそうです。 -
mysqlclient
パッケージをインストールするためにpkg-config
とgcc
が、ランタイムで動作させるためにdefault-libmysqlclient-dev
をインストールしています。
このイメージでも問題なく動作はしますが、
mysqlclient
パッケージのインストールのためだけに必要なパッケージが入っているせいで、このイメージのサイズは「725MB」もあります。
また、ランタイムに本来不要なパッケージが含まれているというのも気になります。それでは、次の章でマルチステージ化してみましょう。
uv流のマルチステージビルド
uvのドキュメントに書かれているマルチステージビルド用のサンプルを参考に、先ほどのイメージをマルチステージ化してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | FROM python:3.13-slim AS builder # uvのインストール COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app # ビルドに必要なパッケージをインストール RUN apt update && apt install -y pkg-config gcc # バイトコードのコンパイルを有効 (起動時間が短縮される傾向がある) ENV UV_COMPILE_BYTECODE=1 # pyproject.tomlとuv.lockのみを参照して依存パッケージのインストール RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-editable --no-group dev # アプリケーションのソースコードをコピー ADD . /app # アプリケーションを --no-editable でインストール RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-editable --no-group dev FROM python:3.13-slim # .venvのみコピー COPY --from=builder --chown=app:app /app/.venv /app/.venv # ランタイムで必要なパッケージをインストール RUN apt update && apt install -y default-libmysqlclient-dev # FastAPIを起動 (--no-editableでインストールされているので、アプリケーションはvenvのsite-packages内にある) CMD ["/app/.venv/bin/fastapi", "run", "/app/.venv/lib/python3.13/site-packages/uv_docker/app.py", "--port", "80", "--host", "0.0.0.0"] |
このイメージのポイント
- ビルド時のみ必要な
pkg-config
とgcc
はbuilderステージでのみのインストール、ランタイムのみで必要なdefault-libmysqlclient-dev
は最終ステージでのみインストールしています。 - 1回目の
uv sync
ではpyproject.toml
とuv.lock
のみを参照し、--no-install-project
オプションを付けることで依存パッケージのみをインストールしています。
これにより、ソースコードに変更が加えられたとしても、依存パッケージをインストールするためのRUN uv sync
はキャッシュが効き、ビルド時間を短縮することができます。
さらに、--mount=type=cache,target=/root/.cache/uv
を指定することで、2回目のuv sync
時にキャッシュを共有するため、ビルドが速くなります。 - 2回目の
uv sync
は、アプリケーションのソースコードがコピーされた状態で実行します。 1回目のキャッシュを使用しつつ実行されるため、依存パッケージのインストール時間はほとんどかかりません。
また--no-editable
を指定することで、プロジェクト自体も.venv
配下のsite-packages
内にインストールされます。こうすることで、アプリケーションに必要なものはすべて.venv
配下に収まるようになります。 - 最終ステージでは、 builderステージからは
.venv
配下のコピーし、ランタイムで必要なパッケージをインストールし、FastAPIを起動します。
このイメージは「423MB」とかなり小さくなりました!
便利オプションの紹介(おまけ)
uv sync --no-editable
はプロジェクト内に変更があっても.venv内に反映されないのですが、
--reinstall
オプションを付けることで強制的に.venv内に再インストールされ、反映されるようになりました。
さいごに
uv流のDockerfileは、初め見た時はよく分からなかったのですが、一つ一つ紐解いていくと効率の良いDockerfileであることが分かり、同時にこのサンプルをベースに各ステージごとに適切な依存パッケージをインストールすることで、最終イメージを小さくすることができました。
uvを使う時はDockerfileにもひと工夫入れてみましょう!