uvで管理するFastAPIプロジェクトの本番イメージのMulti Stage Build
はじめに
Nauhの池田です。
以前はPythonのプロジェクトはpoetryで管理することが多かったのですが、最近はuvを使用することが増えてきました。
FastAPIのバックエンドサービスをKubernetes, CloudRun, ECSなどでホストする際はContainer Imageのサイズをできるだけ削減するために最低限の依存関係だけをインストールしたContainerを用意することが一般的ですが、uvで管理するプロジェクトの場合はどのように実現できるのかを調べたのでその結果をまとめておきます。
uvについて
uv
の特徴としてはRustで開発されており高速に動作することなどがありますが、個人的にPythonのプロジェクトの開発をしていてpoetry
より便利だなと思うのはPythonのバージョンもuv
で管理できることです。
開発環境のPythonのバージョンを固定するためにDevcontainerを利用するなどといった方法もありますが、どうしても開発環境の立ち上げにワンテンポかかるため、ローカルのPythonの仮想環境だけで済むのは楽です。poetry
もpyenvと組み合わせることで似たようなことはできますが、uv
の手軽さには叶わないと思います。
Container Imageのビルド方針
方針
コンテナオーケストレーターでPodを立ち上げる際、Imageのサイズは小さい方がPullが早くなるので有利です。また、同梱するツールが多いと不具合を引き起こす確率も増加します。
Pythonで開発している製品のContainer Imageを作成する際はMulti Stage Buildを利用して、builder環境でPythonの仮想環境を作成し必要な依存パッケージをインストールし、本番Imageに仮想環境と製品コードをコピーして利用するというのが基本的なやり方となります。
プロジェクトの準備
FastAPIのサンプルプロジェクトを準備します。
$ uv init fastapi-uv-image-build --python 3.10.17
$ cd fastapi-uv-image-build
$ uv add fastapi[standard]
今回は検証用に最低限のAPIがあればいいのでmain.py
に以下のようなAPIを実装します。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
このコマンドでlocalhost:8080
にAPIを立ち上げることができます。
$ uv run fastapi run main.py --port 8080
curl
でリクエストを投げてみて、Hello Worldが返ってくればOKです。
$ curl localhost:8080
{"message":"Hello World"}%
Dockerfileの作成
ここから本題であるDockerfileを作成していきます。
まずはベースイメージを選定します。ベースイメージにはDockerHubのPython OfficialのImageを利用するのが便利です。
OSはDebianを選択したほうが無難です。Alpineはglibc
でなくmusl-libc
を採用していることもあり、Debianベースでは発生しないエラーが発生することもあります。
Pythonのバージョンは開発環境で使用しているPythonのバージョンを指定します。開発環境ではローカルに指定のPythonのバージョンがなければuv
が自動でPythonをダウンロードしてきますが、本番イメージにはuv
を含めないためImage内にあらかじめ使用するバージョンのPythonが存在するイメージを利用します。
今回はpython:3.10.17-slim-bookworm
を利用します。
以下のようなDockerfile
を作成します。
FROM python:3.10.17-slim-bookworm AS base
ENV DEBIAN_FRONTEND=noninteractive
FROM base AS builder
COPY /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml .
COPY uv.lock .
ENV UV_COMPILE_BYTECODE=1 \
UV_NO_INSTALLER_METADATA=1 \
UV_LINK_MODE=copy
RUN uv sync --frozen
FROM base AS app
WORKDIR /app
COPY /app/.venv /app/.venv
COPY main.py .
ENV PATH=/app/.venv/bin:$PATH
EXPOSE 8000
CMD ["fastapi", "run", "main.py", "--port", "8000"]
まずはbase
ステージを定義します。今回はないですがapt-get
などでパッケージを入れる必要がある場合はこのステージに処理を追加します。
builder
ではPythonの仮想環境を用意して依存パッケージをインストールします。
この行はuvのオフィシャルガイドに紹介されている方法で、uvのオフィシャルイメージからuv
の実行ファイルをコピーしてくることができます。
COPY /uv /uvx /bin/
pyproject.toml
, uv.lock
をイメージにコピーして
RUN uv sync --frozen
を実行すると仮想環境が作成されて依存パッケージがインストールされます。
最後にapp
ステージにFastAPIの実行に必要なものをコピーして終わりです。
Dockerfile
を作成した上で以下のコマンドを実行すると、Docker Imageをビルドしてサーバーを実行することができます。
$ docker build -t fastapi-sample .
$ docker run --rm -it -p 8000:8000 fastapi-sample:latest
localhost:8000
からAPIにアクセスできるので、以下のようにレスポンスが返ってくれば成功です。
$ curl localhost:8000
{"message":"Hello World"}%
今回作成したイメージのサイズは175MBでした。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
fastapi-sample latest 16dfd8ad9113 16 minutes ago 175MB
仕組み
本番イメージの中身を少し見てみます。
$ docker run --rm -it fastapi-sample:latest bash
root@ac05d2961865:/app# ls -la
total 16
drwxr-xr-x 1 root root 4096 May 15 06:58 .
drwxr-xr-x 1 root root 4096 May 15 06:58 ..
drwxr-xr-x 4 root root 4096 May 15 06:14 .venv
-rw-rw-r-- 1 root root 117 May 15 06:17 main.py
/app
には実行ファイルmain.py
と仮想環境.venv
が存在します。
python
とfastapi
の実行ファイルの場所を確認すると仮想環境内に存在することがわかります。
root@ac05d2961865:/app# which python
/app/.venv/bin/python
root@ac05d2961865:/app# which fastapi
/app/.venv/bin/fastapi
このpython
の実行ファイルは/usr/local/bin/python3
へのシンボリックリンクになっています。
root@ac05d2961865:/app# ls -l $(which python)
lrwxrwxrwx 1 root root 22 May 15 06:14 /app/.venv/bin/python -> /usr/local/bin/python3
uv
は仮想環境を作成する際に指定のバージョンのPython interpreterがPATH
に存在するかを確認し、存在したらそのPython interpreterを利用し、
見つからなければそのバージョンのPython interpreterをダウンロードしてきます。
よってあらかじめ環境内に目的のバージョンのPython interpreterを持つイメージをベースイメージとすることでinterpreterのダウンロードが発生しないようにしています。
Pythonのバージョン周りの正確な動作については公式ドキュメントを参照してください。
まとめ
本記事では、uvで管理するFastAPIプロジェクトのコンテナイメージをMulti Stage Buildを使って効率的に作成する方法を紹介しました。この方法の主なポイントは以下の通りです:
-
uvの活用: Rustで実装された高速なパッケージマネージャである
uv
を利用することで、開発環境でのPythonバージョン管理も含めた効率的な環境構築が可能になります。 -
Multi Stage Buildの採用: ビルド環境と実行環境を分離することで、最終的なイメージサイズを175MB程度まで削減できました。これにより、コンテナのデプロイ速度の向上や運用コストの削減が期待できます。
-
効率的な依存関係管理:
uv sync --frozen
コマンドで依存パッケージを厳密にインストールし、本番環境に必要な最小限のコンポーネントだけを含めることができます。 -
uvの実行ファイルの取得: uvのオフィシャルイメージから実行ファイルをコピーするという手法を使うことで、ビルド環境でuvを簡単に利用できます。
この方法は他のPythonフレームワークを使用したプロジェクトでも応用できるので、ぜひ試してみてください。今回紹介したパターンを活用することで、軽量かつ効率的なコンテナイメージの作成が可能になると思います。
Discussion