前段
前回の2024年8月の自分の記事 から早1年と2か月。docker uv python
などでググると、uvが安定していなかった過去のやり方を記述した検索結果が上位にひっかかるため、現時点でのベストプラクティス(自称)をここに記すこととする。
Disclamer
この記事の内容は、私個人の意見や見解であり、私が所属する組織の公式な立場、方針、意見を反映するものではありません。この記事の内容について、組織はいかなる責任も負いません。
はじめに
https://github.com/astral-sh/uv-docker-example
https://docs.astral.sh/uv/guides/integration/docker/
まずはこの2つのサイトを読めば、これから下の内容を参照する必要がなくなります。
方針
- Debian 13 (trixie) が2025年8月にリリースされていたのでこれをベースとする
- python は system python on docker を使い、uv managed python は使わない
- docker は multi-stage buildを利用する
- application は noroot user で実行する
- チームで Mac と WSL2 が混在していても volume オプションの書き込みで問題なく動くようにする
前提
streamlit の実行を想定する。以下のフォルダ構成
$ tree
.
├── compose.yaml
├── Dockerfile
├── pyproject.toml
├── src
│ └── main.py
└── uv.lock
結論
Dockerfile
# syntax=docker/dockerfile:1
ARG PY_VERSION="3.13.9"
ARG UV_VERSION="0.9.4"
# UV image stage
FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv
# -- Builder Stage --
FROM python:${PY_VERSION}-slim-trixie AS builder
ENV UV_SYSTEM_PYTHON=1 \
UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1
RUN --mount=from=uv,source=/uv,target=/bin/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=cache,target=/root/.cache/uv \
uv export --frozen --no-dev --no-editable | uv pip install -r -
# prod
FROM python:${PY_VERSION}-slim-trixie AS prod
ARG PY_VERSION
ARG PY_VERSION_MINOR=${PY_VERSION%.*}
ENV PYTHONUNBUFFERED=1
WORKDIR /src
COPY --from=builder \
/usr/local/lib/python${PY_VERSION_MINOR}/site-packages \
/usr/local/lib/python${PY_VERSION_MINOR}/site-packages
# ↓need to copy cli python command
COPY --from=builder /usr/local/bin/streamlit /usr/local/bin/streamlit
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG HOST_UID=1000
ARG HOST_GID=1000
# Create a non-root user and group
RUN <<EOF bash -eux
if ! getent group ${HOST_GID} > /dev/null; then
groupadd -g ${HOST_GID} appgroup;
fi
useradd -u ${HOST_UID} -g ${HOST_GID} -m appuser
EOF
COPY --chown=appuser:appgroup src/ .
# Switch to the non-privileged user to run the application.
USER appuser
# Expose the port that the application listens on.
EXPOSE 8501
# Run the application.
CMD ["streamlit", "run", "main.py", "--browser.gatherUsageStats=false", "--server.headless=true"]
compose.yaml
services:
server:
build:
context: .
args:
# 環境変数をビルド時の引数に渡す
- HOST_UID=${HOST_UID}
- HOST_GID=${HOST_GID}
image: uv-python-docker-example
container_name: uv-python-docker-example
ports:
- 8501:8501
volumes:
# ローカルのカレントディレクトリをコンテナの /src にマウント
- ./src:/src
実行
HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose up --build
compose を使わない場合は
docker build --build-arg HOST_UID=$(id -u) --build-arg HOST_GID=$(id -g) -t uv-python-docker-example .
docker run --rm -p 8501:8501 -v ./src:/src uv-python-docker-example
本番環境では HOST_UID
や HOST_GID
の指定は不要(デフォルトの1000が使用される)
Dockerfileの解説
# syntax=docker/dockerfile:1
docker syntaxディレクティブの有効化
# UV image stage
FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv
マルチステージビルドにて uv イメージの指定。
ENV UV_SYSTEM_PYTHON=1 \
builderステージにて uv でシステム python を利用する環境設定。なお、このオプションは uv sync
, uv lock
, uv add
などには効果がないので、後に uv pip install ...
を使うこととする。
UV_LINK_MODE=copy \
これは cache (docker image 実行では /root/.cache/uv
) に保存した package file をどのように実行パス( UV_SYSTEM_PYTHON=1
で設定しているので /usr/local/
に連携するか。copy
, hardlink
, clone
, symlink
があり、Dockerfileでは cache からの確実な連携により copy
が望ましい。
RUN --mount=from=uv,source=/uv,target=/bin/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=cache,target=/root/.cache/uv \
uv export --frozen --no-dev --no-editable | uv pip install -r -
マルチステージビルドの肝。まず1行目でuvバイナリをマウント。2,3行目で対象ファイルをbind、4行目でcacheを指定、5行目で requirements.txt
を作成せずパイプで渡してインストール。--frozen
は uv.lock を更新せずそのまま使う。 --no-dev
は pyproject.toml の dependency-groups.dev
をインストールしない。 --no-editable
はプロジェクトを編集不可モードでインストール。requirements.txt を作成せずパイプで渡して uv pip install ...
する。
prodステージ
ARG PY_VERSION
ARG PY_VERSION_MINOR=${PY_VERSION%.*}
FROMコマンドは新たな変数スコープを作成する。最初の FROM より前に出現する ARG はグローバルスコープとなる。
一方でグローバルスコープの ARG はマルチステージの各 FROM スコープ(FROMの内側)へは自動的に引き継がれないため、
FROMの内側で再宣言する必要がある(値は引き継がれる)。
ARG HOST_UID=1000
ARG HOST_GID=1000
# Create a non-root user and group
RUN <<EOF bash -ex
if ! getent group ${HOST_GID} > /dev/null; then
groupadd -g ${HOST_GID} appgroup;
fi
useradd -u ${HOST_UID} -g ${HOST_GID} -m appuser
EOF
まずはnoroot user 対応。WSL2上で開発する際に、volumeオプションでローカル同期した時にdocker側でファイル作成した際にroot実行されたファイルは通常ユーザーと権限が異なり編集や削除等ができなくなる。そのためローカル実行時には compose コマンド実行のように HOST_UID
等の環境変数を指定して実行する。Macではユーザー所属のGID=20がDocker image上のubuntuでUIDとして存在しているのでその確認プロセスを入れている。
RUN の複数行はヒアドキュメントが使える。-eux
にてエラー停止、未定義変数エラー、ログ詳細を設定。
COPY --chown=appuser:appgroup src/ .
アプリケーションの権限はrootではなく --chown でユーザー権限を指定するのがセキュリティー的に望ましい。
皆様のお役に立てれば幸いです。
qiita の uv(astral-sh)
タグが広まってきてなによりです。
以上