uv on Dockerでvenv仮想環境を作成しないPython開発用コンテナを構築する

## はじめに

私は現在Azure上で実行される生成AIアプリケーションの開発を行っていますが、 `App Service・Functions` へデプロイを行うにはコンテナを利用しないととてもじゃないがやってられないと感じています。

バックエンド側には生成AIや他ML等と相性が良いPythonを利用しているのですが、最近流行りのモジュール等管理ツール `uv` をコンテナで利用したい！動作速いし、依存関係も勝手に記載してくれる！・・・でも勝手に `.venv` が作成されるのが嫌だなと思った次第です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/432253/33e8f312-819f-45c2-85a9-87c925f7490f.png)


そもそもが仮想環境であるDockerコンテナに `venv` 仮想環境をさらに作成する意味もありませんし、余計な管理コストや技術負債は増やしたくないものです。

### 作りたい環境のイメージ

- pythonの開発用Dockerコンテナ
- `uv` が高速かつ依存関係の自動記載等便利ため、開発・マルチステージBuilder用に使いたい
- **`.venv` を作りたくない**
- Docker内のシステムPythonのインストール先にモジュールをインストールしたい
- 依存関係が記載されたファイル `uv.lock` はプロジェクト内のフォルダに配置しておきたい
- 拡張機能などを自動でインストールしたいため、 `Devcontainer` 拡張機能を利用したい

## 結論

下記の環境変数をコンテナに設定する。
- `UV_SYSTEM_PYTHON=1`
    - systemにインストールされているPythonを利用する
- `UV_PROJECT_ENVIRONMENT=/usr/local/`
    - 使用するPython環境を `/usr/local` に設定し、 `venv` 環境を作成しない

## 事前準備

- Docker開発環境( `Docker Desktop` `Docker CLI` 等)の構築
- `Visual Studio Code` のインストール
- `Dev Containers` 拡張機能のインストール

## 注意

- 筆者の環境は`Windows 11 24H2`なのでmacやLinuxでは多少動作が異なるかもしれません
- 本記事のDockerコンテナはRootで利用しているため、本場環境では作業用ユーザーの作成など行ってください
- 本番用コンテナに `uv` がインストールされているべきではないため、本番環境の構築にはマルチステージビルドを利用してください

## 開発コンテナの構築

#### ディレクトリ構成

``` Powershell
project-root>tree /F

project-root
│  .python-version
│  Dockerfile
│  pyproject.toml
│  README.md
│  (ruff.toml)
│  uv.lock
│  
├─.devcontainer
│      devcontainer.json
│      
└─src
        hello.py
```

`uv init` 後の想定されるディレクトリ構成です。 `ruff.toml` は `pyproject.toml` と同一にして問題ありません。

#### Dockerfile

``` Dockerfile
FROM python:3.11.9-slim-bookworm AS develop

WORKDIR /app

# uvの破壊的バージョンアップ対策として特定のバージョンを指定してインストール
COPY --from=ghcr.io/astral-sh/uv:0.5.3 /uv /uvx /bin/

ENV UV_SYSTEM_PYTHON=1 \
    UV_PROJECT_ENVIRONMENT=/usr/local/

RUN --mount=type=cache,target=/var/lib/apt,sharing=locked \
    --mount=type=cache,target=/var/cache/apt,sharing=locked \
    apt-get update

COPY . .

# 依存関係の解決(uv init後、uv.lock生成後にコメントアウト解除)
# RUN --mount=type=cache,target=/root/.cache,sharing=locked \
#     uv sync --frozen

```

今回ベースとなるコンテナイメージは、`Debian bookworm slim`の `python 3.11.9` コンテナを利用しています。 `Azure App Service` ・ `Azure Functions` のサポートするPythonバージョンが最近まで `3.11` までだった名残です。
uvはまだ登場したばかりで、ときたま破壊的なアップデートが行われるため、現状ですとバージョンを指定してインストールしておけばチームメンバー間での差異もなくなりよさそうです。
依存関係の解消部分は、 `uv.lock` ファイルが存在しないといけないため、 `uv init` を行った後にコメントアウトを解除してください。

#### Devcontainer.json

``` JSON
{
  "name": "My Dev Container",
  "workspaceFolder": "/app",
  "workspaceMount": "source=${localWorkspaceFolder},target=/app,type=bind,consistency=cached",
  "build": {
    "dockerfile": "../Dockerfile"
  },
  "settings": {},
  "extensions": [
    "ms-python.python",
    "ruffcharliermarsh.ruff"
  ]
}

```
ホストのカレントワークスペースフォルダをコンテナ内 `/app` にマウントしています。`uv` を使うなら同じくRustで作られたPythonのLinter・Formatter `Ruff` が便利です。

## 開発コンテナの起動

1. `Docker Desktop` を起動します
1. F1で `Devcoitainers: Reopen in Container` をクリックし、開発用コンテナを起動します

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/432253/5d5fc885-0e76-4ce6-93f6-93a106daf276.png)

1. プロジェクトの初回起動時は、uvの初期設定コマンドを実行し、 `pyproject.toml` などを生成します

    ``` bash
    user:/app# uv init
    ```

1. プロジェクトで利用するpythonモジュールをインストールします

    ``` bash
    user:/app# uv add requests
    Resolved 6 packages in 5ms
    Prepared 5 packages in 207ms
    Installed 5 packages in 7ms
     + certifi==2025.1.31
     + charset-normalizer==3.4.1
     + idna==3.10
     + requests==2.32.3
     + urllib3==2.3.0
    ```

1.uv.lockが生成されたら次回以降はDockerfileの `uv sync` 部分のコメントアウトを解除すれば、Dev ContainerでRebuildを行った際にも勝手に依存関係を解消してくれます。他メンバーがCloneして使う際もDockerfileで `uv sync` しておけば勝手に依存関係は解消されます。


各モジュールはシステムのPython `user/local/site-package` にインストールされており、 `uv pip list` でも、デフォルトの `pip list` でも同じ個所を参照してくれています。

``` bash
user:/app# uv pip list
Using Python 3.11.9 environment at /usr/local
Package            Version
------------------ ---------
certifi            2025.1.31
charset-normalizer 3.4.1
idna               3.10
pip                24.0
requests           2.32.3
setuptools         65.5.1
urllib3            2.3.0
wheel              0.44.0
```

``` bash
user:/app# pip list
Package            Version
------------------ ---------
certifi            2025.1.31
charset-normalizer 3.4.1
idna               3.10
pip                24.0
requests           2.32.3
setuptools         65.5.1
urllib3            2.3.0
wheel              0.44.0
```
プロジェクトのルートに `.venv` が作られず、Dockerコンテナ内のシステムPythonにパッケージがインストールされるようになりました！解決！

本番用コンテナを作成するときは、Composeのマルチステージビルドを利用して `AS builder`ステージで`uv sync --frozen --no-dev` を行い `site-packages`を `AS production` にコピーすれば行けそうですね。

## 参考

https://github.com/astral-sh/uv/pull/6834

https://docs.astral.sh/uv/