こんにちは!開発現場の裏側で、日々「どうすればもっとコードを書く時間が作れるか」「どうすればビルドやデプロイのストレスをゼロにできるか」ばかりを考えている先輩エンジニアです。
Pythonのコンテナイメージを作っていて、こんな絶望感を味わったことはありませんか?
「ただのWeb APIサーバーなのに、イメージサイズが1GBを超えている……」
「セキュリティスキャンをしたら、使ってもいないOSパッケージの脆弱性が山のように出てきた……」
これ、Python開発者なら誰もが一度は通る「コンテナ肥大化の呪い」なんです。重たいイメージは、CI/CDのビルド時間を食いつぶし、KubernetesのPod起動を遅らせ、無駄なストレージ費用を発生させます。
今回は、Rust製超高速パッケージマネージャー `uv` を使い、マルチステージビルドの複雑さすら不要なレベルで、Pythonの実行環境を極限まで小さく、そして安全にするテクニックを伝授します。
これをマスターすれば、毎日のコンテナビルドとデプロイが驚くほど軽快になり、「お、なんか仕事できるエンジニアっぽいな」と周囲を唸らせることができますよ。さあ、一緒にその扉を開けてみましょう!
—
1. そもそもなぜPythonのコンテナは重くなるのか?
まず、敵を知ることから始めましょう。普段、私たちが何気なく書いている次のようなDockerfileを思い出してください。
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD [“python”, “main.py”]
一見、シンプルで問題なさそうに見えますよね。しかし、このベースイメージ(`python:3.11`)の実態は、DebianなどのフルスペックのLinuxディストリビューションです。コンパイルツール(gccなど)、シェル、パッケージマネージャー、そしてPythonのヘッダーファイルなど、「アプリケーションの実行には1バイトも必要ない開発用アセット」が満載されています。
結果として、ただの「Hello World」レベルのアプリですら数百MB〜1GB近い肥大化したコンテナが完成します。
解決の鍵:`uv` と `distroless` の融合
ここで登場するのが、Astral社が開発した爆速パッケージマネージャー `uv` です。`uv` は単に `pip` の何十倍も速いだけでなく、「依存関係だけを綺麗に別のディレクトリへ切り出す」という極めて強力な機能を持っています。
これを利用し、Googleが提供する「distroless(ディストロールレス)イメージ」にアプリと依存関係だけをポンと放り込む。これが、今回目指す「極小コンテナ」の正体です。
—
2. 環境準備とツールの基礎セットアップ
まずは、ローカルのPC(またはビルド環境)に `uv` をインストールし、今回の実験用プロジェクトをセットアップしましょう。
`uv` のインストール
macOSやLinuxであれば、公式のインストーラーを1発叩くだけです。
uvの公式インストーラーを実行
curl -sSf https://astral.sh/uv/install.sh | sh
インストールが完了したら、ターミナルを再起動するか、パスを通してからバージョンを確認します。
uv –version
出力例: uv 0.x.x (正常にインストールされていればOK)
※驚くほど一瞬でインストールが終わります。このスピード感、すでにワクワクしませんか?
最小構成のプロジェクトを作成する
適当なディレクトリを作り、そこで `uv` を使ったプロジェクトを初期化します。
mkdir uv-distroless-demo
cd uv-distroless-demo
仮想環境を作成しつつ、プロジェクトを初期化
uv init –app
これで、プロジェクトのルートに `pyproject.toml` や `main.py` が自動生成されます。
—
3. 精度高い「HelloWorld」動作確認コードの準備
今回は、軽量なWebフレームワークである `fastapi` と、本番サーバーである `uvicorn` を使った簡単なAPIサーバーを例にします。
依存関係の追加
`uv` を使って、一瞬でライブラリを追加しましょう。
内部で仮想環境を作りながら高速にインストールしてくれます
uv add fastapi uvicorn
アプリケーションコード(`main.py`)の作成
`main.py` を次のように書き換えます。これが今回の動作確認用のコードです。
from fastapi import FastAPI
FastAPIのインスタンス化
app = FastAPI()
@app.get(“/”)
def read_root():
“””
ヘルスチェックやコンテナの動作確認用のエンドポイント
“””
return {“message”: “Hello from uv-powered distroless container!”}
if __name__ == “__main__”:
import uvicorn
# ローカル検証用の起動設定
uvicorn.run(app, host=”0.0.0.0″, port=8000)
ローカルで動かす場合は、以下のコマンドで一発起動できます。
uv run python main.py
ブラウザで `http://localhost:8000` にアクセスし、JSONが返ってくることを確認してください。ここまでは序の口です。
—
4. 肝となる技術:`uv pip install –target` による依存関係の分離
通常、Pythonのライブラリは仮想環境(`.venv` など)の中にごっそりインストールされます。しかし、後述する `distroless` イメージには、OSのパッケージマネージャーはもちろん、`activate` スクリプトを実行するような bash すら入っていません。
ここで `uv pip install –target` の出番です。
このコマンドを使うと、「仮想環境を作らずに、指定した任意のディレクトリ(例: `/app/dist`)に、純粋なPythonパッケージのソースコード群だけを直接配置(フラットに抽出)」することができます。
ローカルで以下のコマンドを試してみてください。
–target /tmp/packages を指定して、依存関係を指定フォルダに直接書き出す
uv pip install –target /tmp/packages fastapi uvicorn
`/tmp/packages` の中を覗くと、`fastapi` や `pydantic` などのフォルダが綺麗に並んでいるはずです。「このフォルダさえコンテナに持って行ければ、Pythonの実行環境さえあれば動く」という状態を作れたわけです。
—
5. 実戦投入:極小コンテナを作るDockerfileの全貌
それでは、いよいよ本記事の核心である Dockerfile を作成します。
今回はマルチステージビルドを使いますが、ビルド用ステージで `uv` を使って一瞬でビルドを済ませ、最終的な実行用ステージには Googleの Distroless (Python 3.11) を採用します。
プロジェクトのルートに `Dockerfile` を作成し、以下のコードを記述してください。
==========================================
ステージ1: ビルドステージ (uvによる高速ビルド)
==========================================
ここでは通常のPythonイメージを使って依存関係をビルドします
FROM python:3.11-slim AS builder
1. ビルドを高速化するため、uvのバイナリを公式イメージから直接コピー
COPY –from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
2. 依存関係の定義ファイルをコピー
COPY pyproject.toml uv.lock ./
3. 仮想環境を作らず、/app/site-packages に直接依存パッケージをインストールする
(–no-devでプロダクションに必要なパッケージのみに絞る)
RUN uv pip install –system –target /app/site-packages –no-dev -r pyproject.toml
4. アプリケーションのソースコードをコピー
COPY main.py .
==========================================
ステージ2: ランタイムステージ (究極の軽量・安全な実行環境)
==========================================
Googleが提供する軽量・セキュアなdistrolessイメージ(シェルや不要なツールが一切ない)
FROM gcr.io/distroless/python3-debian12:nonroot
WORKDIR /app
5. ビルドステージで作成した「純粋なライブラリ群」と「ソースコード」だけをコピー
COPY –from=builder /app/site-packages /app/site-packages
COPY –from=builder /app/main.py /app/main.py
6. distrolessのnonrootユーザー権限で実行するため、Pythonがモジュールを探せるパスを通す
ENV PYTHONPATH=/app/site-packages
7. コンテナが待ち受けるポートを指定
EXPOSE 8000
8. シェルが存在しないため、実行コマンドは配列形式(exec形式)で直接指定する
CMD [“main.py”, “–host”, “0.0.0.0”, “–port”, “8000”]
このDockerfileが天才的な理由
1. 圧倒的なビルドスピード: `uv` のキャッシュと並列ダウンロードのおかげで、依存関係のインストールが数秒で終わります。
2. シェルの排除(Distroless): `gcr.io/distroless/python3-debian12:nonroot` には、`bash` や `sh` すら含まれていません。これにより、攻撃者がコンテナ内に侵入してシェル芸で不正コマンドを実行する余地を完全に断ち切ります(セキュリティ監査で大絶賛されるポイントです)。
3. 無駄な容量の削減: Debianの重たいパッケージやビルドツールが最終成果物に一切含まれないため、イメージサイズを驚異の数十MB台(通常イメージの1/5以下)にまで圧縮できます。
—
6. ビルドと動作確認の儀式
それでは、実際にコンテナをビルドして、その軽さと動作を自分の目で確かめてみましょう。
コンテナのビルド
以下のコマンドで Docker イメージをビルドします。
docker build -t uv-distroless-app .
どうですか? `uv` のおかげで、ビルドが瞬時に完了したはずです。
イメージサイズの確認
どれくらい軽くなったか、サイズを確認してみましょう。
docker images uv-distroless-app
出力結果を見て思わずニヤリとしてしまったのではないでしょうか? 従来のPython公式イメージベースで作った場合と比べ、劇的にサイズが削ぎ落とされていることが確認できます。
コンテナの起動とAPIテスト
コンテナをバックグラウンドで起動します。
docker run -d -p 8000:8000 –name my-fastapi-container uv-distroless-app
別のターミナルから、APIを叩いてみましょう。
curl http://localhost:8000/
期待されるレスポンス:
{“message”:”Hello from uv-powered distroless container!”}
見事にレスポンスが返ってきました!
最後に、コンテナを停止・削除して後片付けをしておきます。
docker stop my-fastapi-container
docker rm my-fastapi-container
—
7. おわりに:現場のエンジニアライフを劇的に変えるために
今回は、`uv` の `–target` 機能と `distroless` イメージを組み合わせた、Pythonコンテナの極限の軽量化テクニックを解説しました。
「コンテナのサイズが小さくなる」ということは、単にストレージ容量が節約できるだけではありません。
- CI/CDパイプラインのビルド&プッシュ時間が短縮され、デプロイの待ち時間が消える。
- KubernetesなどのオーケストレーターでのPod起動が高速化し、オートスケーリングの追従性が劇的に向上する。
- 不要なパッケージが含まれないため、脆弱性スキャンのアラート(CVE)が激減し、セキュリティチームとの無駄なやり取りがなくなる。
毎日の開発において、「遅い」「重い」というストレスが一つ消えるだけで、コードを書く楽しさは何倍にも膨れ上がります。ぜひ、あなたの次回のプロジェクトやマイクロサービスのデプロイに、この `uv` × `distroless` のコンボを取り入れてみてください。
あなたの開発ライフが、より軽快でエキサイティングなものになることを応援しています!