Closed4

[翻訳] uv による本番環境向け Docker コンテナ

ピン留めされたアイテム
まちゅけんまちゅけん

Hynek 氏の Production-ready Docker Containers with uv の記事を機械翻訳して転載します。

https://hynek.me/articles/docker-uv/

本文章は執筆時のおおよそ 2024-09-04T10:07:09.435795+09:00 辺りの版です。 翻訳元が更新される可能性があります。

まちゅけんまちゅけん

uv による本番環境向け Docker コンテナ

2024-08-28

uv 0.3.0 では、クロスプラットフォームで使用可能なロックファイル uv.lock のサポートを含む多くの素晴らしい機能が追加されました。開発者の体験(DX)を明らかに重視した内容であり、Python の DX における大きな進歩を感じさせます。しかし、現時点では、ベストプラクティスの Docker コンテナ(および tox/Nox ですが、それについてはまた別の機会に)の中で uv を使用する際にやや厄介な問題があります。それは、uv sync コールにおいてターゲットとなる仮想環境を選択する機能が欠けていること、または uv install において uv.lock をサポートしていないことです。

なお、プロダクション環境では、Docker のワークフローから以下の特性を確保したいと考えています:

  1. マルチステージビルド: ビルドツールを含まないイメージを作成するため。

  2. 適切なレイヤリング: ビルドを高速化するためです。レイヤーは、変更される可能性が低い順に追加することで、キャッシュをできるだけ長く活用できるようにするべきです。

    これにより、依存関係のインストール(uv.lock 内のもの)とアプリケーションのインストール(あなたが書いたコード)は厳密に分けるべきです。開発中は、コードの変更が依存関係よりも頻繁に発生するためです。

  3. ボーナス: ビルドキャッシュマウント: たとえば、依存関係のレイヤーを再作成する必要がある場合に、ホイールファイルを再ビルドしなくても済むようにするため。

  4. ボーナス: Python ファイルのバイトコンパイル: コンテナの起動時間を短縮するためです。

まちゅけんまちゅけん

私が好んで行うのは、アプリケーションを /app ディレクトリ内に構築し、その仮想環境をランタイムコンテナに丸ごとコピーすることです。これには多くの利点があり、異なる Python バージョンのベースコンテナを共有できる点や、仮想環境が binlibshare ディレクトリを含むため、自然な形でアプリケーションコンテナとして利用できる点が挙げられます。

しかし、uv では少し厄介です。なぜなら、uv installuv.lock ファイルをサポートしておらず、uv sync は常にカレントディレクトリにある .venv という仮想環境を使用するからです。もし存在しなければ、uv sync は透過的にそれを作成しますが、これにより空のコンテナをデプロイしてしまう可能性があり、これがなかなか面白い問題を引き起こします。

この問題は、.venv/app の下に配置し、$PATH 変数を調整して、uv pip を使用してアプリケーションをインストールすることで回避できます。

それでは、ウェブアプリケーションのコンテナを一緒に作ってみましょう。私はプロダクション環境では uWSGI を使いません。なぜなら、uWSGI は現在「メンテナンスモード」として管理者によって宣言された、ほぼ休止状態の C 言語のコードの塊だからです。しかし、ここではあえて使います。なぜなら、uWSGI は追加の依存関係を必要とし、現実のプロジェクトでも遭遇する可能性が高いため、ビルドの複雑さを増すからです[1]

脚注
  1. ベースイメージとして Ubuntu を使用していますが、これは私が慣れているからです。Docker 以外の環境で独自のベースイメージと PPA を使用しているため、プラットフォームを統一するのが理にかなっています。お好みのイメージを使ってください。ただし、お願いですから Alpine ベースのものは避けてください。数メガバイトを節約する代償として引き起こす問題は、その価値がありません。 ↩︎

まちゅけんまちゅけん
# syntax=docker/dockerfile:1.9
FROM ubuntu:noble as build

### ビルド準備開始
### これを別のビルドコンテナに分けることで、再利用性が向上します。

RUN <<EOT
set -ex

apt-get update -qy
apt-get install -qyy \
    -o APT::Install-Recommends=false \
    -o APT::Install-Suggests=false \
    build-essential \
    ca-certificates \
    curl \
    python3-setuptools \
    python3.12-dev
EOT

# セキュリティ意識の高い組織は、自分たちで uv をパッケージ化/レビューすべきです。
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# - ハードリンクが使えない場合に uv が警告しないようにし、
# - uv にパッケージをバイトコンパイルさせてアプリケーションの起動を高速化し、
# - uv が誤って隔離された Python ビルドをダウンロードしないようにし、
# - 最後に、使用する Python を選択します。
ENV UV_LINK_MODE=copy \
    UV_COMPILE_BYTECODE=1 \
    UV_PYTHON_DOWNLOADS=never \
    UV_PYTHON=python3.12

### ビルド準備終了 -- ここから Dockerfile が始まります。

# 仮想環境を準備します。
# これは上記の Python バージョンが変更されるまでキャッシュされます。
RUN --mount=type=cache,target=/root/.cache \
    set -ex \
    && uv venv /app/.venv

# アプリケーション自体を除いた依存関係を同期します。
# このレイヤーは `uv.lock` または `pyproject.toml` が変更されるまでキャッシュされます。
COPY pyproject.toml /app
COPY uv.lock /app
RUN --mount=type=cache,target=/root/.cache \
    set -ex \
    && cd /app \
    && uv sync --frozen --no-install-project

# 依存関係なしでアプリケーションをインストールします。
# `/src` はランタイムコンテナにコピーされません。
# アプリケーションが適切な Python パッケージでない場合は、この部分を省略してください。
COPY . /src
RUN --mount=type=cache,target=/root/.cache \
    set -ex \
    && uv pip install --python=/app/.venv --no-deps /src


##########################################################################

FROM ubuntu:noble

# アプリケーションの仮想環境を検索パスに追加します。
ENV PATH=/app/.venv/bin:$PATH

# アプリケーションを root ユーザーとして実行しないでください。
RUN set -ex \
    && groupadd -r app \
    && useradd -r -d /app -g app -N app

ENTRYPOINT ["/docker-entrypoint.sh"]
# 詳細は <https://hynek.me/articles/docker-signals/> を参照してください。
STOPSIGNAL SIGINT

# ランタイム依存関係がビルド時の依存関係と異なることに注意してください。
# 特に、uv は含まれていません!
RUN <<EOT
set -ex

apt-get update -qy
apt-get install -qyy \
    -o APT::Install-Recommends=false \
    -o APT::Install-Suggests=false \
    python3.12 \
    libpython3.12 \
    libpcre3 \
    libxml2

rm -rf /var/lib/apt/lists/*
EOT

COPY docker-entrypoint.sh /
COPY uwsgi.ini /app/etc/uwsgi.ini

# 事前に構築された `/app` ディレクトリをランタイムコンテナにコピーし、
# 所有権を一度にユーザー app とグループ app に変更します。
COPY --from=build --chown=app:app /app /app

# アプリケーションが上記で pip インストールされた適切な Python パッケージでない場合、
# この部分でアプリケーションをコンテナにコピーする必要があります:
# COPY . /app/whereever-your-entrypoint-finds-it

USER app
WORKDIR /app

# 完全にオプションですが、私は構築した内容を調べるためにこれを好んで使用します。
RUN set -ex \
    && python -V \
    && python -Im site

これ、そんなに悪くない感じがするけど、大したことじゃないの?

本当に大したことはないんです!ただ、仮想環境を /app/.venv に隠しておくよりも、仮想環境が /app/ 自体になって、その中に binlibshare がある方が少しだけ使いやすいというだけです[1]。また、私はスクリプトや Dockerfile で絶対パスを使うのが好きなんですが、/app/.venv/bin/gunicorn よりも /app/bin/gunicorn の方が言いやすいんです。

だから、#5229 を支持します – 自分のワークフローがネイティブにサポートされるまで待てますが、これが実験してみたい人にとって役立つことを願っています!


P.S. Docker についてもっと助けが必要なら、私の友人 Itamar Turner-Trauring の素晴らしいリソースを参照してください。

P.P.S. Dev Container (訳注[2]) については聞かないでください – 私は知らないし、知りたくもありません。開発で Docker コンテナを使うことを強制された瞬間、私はヤギの飼育 (訳注[3]) に切り替えます。

脚注
  1. 以前のバージョンの記事では、/app/.venv/bin から /app/bin へのシンボリックリンクを追加することを提案していましたが、それには Python が site を発見する方法に予期しない副作用があったため、削除しました。 ↩︎

  2. 訳注: VS Code や GitHub Spaces などにおいて提供されている開発コンテナの機能です: https://code.visualstudio.com/docs/devcontainers/containers ↩︎

  3. 訳注: この文脈では、著者が開発環境で Docker を使うことへの不満や抵抗を強調するために、極端な例として「ヤギの飼育」という、全く異なる職業やライフスタイルに切り替えることを冗談めかして言っています。つまり、「開発で Docker を使わなければならないほど嫌なら、むしろプログラミングを辞めて別のことをする」という皮肉やユーモアの表現です。 ↩︎

このスクラップは3ヶ月前にクローズされました