Streamlit on uv + Docker 開発スタック

# はじめに

最近は Python での環境構築に [uv](https://docs.astral.sh/uv/) を気に入って使っている。
そこで、Streamlit で作成した Web サービスを Docker に囲って出す際に、uv をベースとしたバージョン管理をしつつ公開する方法はどうすればいいかを実際に手を動かしながらやるということをまとめてみる。

# uv プロジェクトの作成

まずは uv プロジェクトを作成するところから。

## プロジェクト作成

```powershell
uv init streamlit-uv-docker
```

## streamlit の追加

Streamlit を依存関係に追加する。

```powershell
uv add streamlit
```

## 開発用のモジュールの追加

こちらは、Docker でリリース環境として扱う際には含まないもの。
例えば、フォーマッタやリンタ、テストツールなどをこちらの方にインストールする。

```powershell
uv add --dev ruff
```

## 結果の確認

### `pyprpoject.toml`

`pyproject.toml` の中は次のようになっていると思う。

```toml:pypyroject.toml
[project]
name = "streamlit-uv-docker"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "streamlit>=1.40.2",
]

[dependency-groups]
dev = [
    "ruff>=0.8.1",
]
```

### Streamlit の起動確認

```powershell
uv run streamlit hello
```
![2024-12-01 17.19.18 localhost fc6bfcbaeadc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/4bfcca24-a158-1ca0-b0f9-e11c246834ce.png)

# アプリの作成

それではベースとなるアプリを作成していく。
uv のデフォルト環境は `hello.py` がおかれているだけ。
ここに実際に記述していってもいいが、ファイルの増加やファイル名がそもそも適切でないことから階層を作り直す。

```
root
  ├─ .venv
  ├─ .python-version
  ├─ app
  │  └─ app.py
  ├─ pyproject.toml
  ├─ README.md
  └─ uv.lock
```

```python:__init__.py
from . import main

main.main()
```

```python:main.py
import streamlit as st

def main():
    st.title("Python + uv + Streamlit + Docker!")
```

さて、ではこの構成で起動してみる。

```powershell
uv run streamlit run app\app.py
```

![2024-12-01 18.04.50 localhost 6a1d06233935.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/ff31cb10-6bf6-0f00-9bf4-06577af91fc1.png)

# Docker 環境の構築

uv 側は、uv インストール済みの Docker イメージを公開してくれている。
まずはこれを利用することを考える。

https://docs.astral.sh/uv/guides/integration/docker/#getting-started

## Dockerfile の作成

```docker:Dockerfile
FROM python:3.12-slim-bookworm

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ADD . /app

WORKDIR /app

RUN uv sync --frozen

CMD ["uv", "run", "streamlit", "run", "app.py"]
```


:::note info
**uv を地でインストールする**
通常のインストールプロセスと同様に uv をインストールする方法もある。
通常の Python 環境等の Dcoker イメージを使用した場合は、以下を上記の COPY 部分の代わりに記述する。

```docker:Docker
FROM python:3.12-slim-bookworm

# レポジトリ情報を更新 && curl をインストールして有効にしておく
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates

# 最新のインストーラーをダウンロード
ADD https://astral.sh/uv/install.sh /uv-installer.sh

# インストーラーを使ってインストールし、インストーラーを削除
RUN sh /uv-installer.sh && rm /uv-installer.sh

# PATH 環境変数にパスが通ってるかを確認
ENV PATH="/root/.local/bin/:$PATH"
```
:::

## .dockerignore で同期不要なものに対する対応

上記までローカルで作業していたため **別環境の .venv 環境** が作成されてしまっているので、Docker 処理でそれごとコピーされてしまうのを `.dockerignore` ファイルを作成することによって防ぐ。

```config:.dockerignore
# Ignore the virtual environment
.venv
```

:::note warn
**TROUBLESHOOT: .venv を ignore しないと？**
次のようなエラーが発生する。.venv は、プラットフォームの依存も含めた仮想環境なのを忘れずに対処をしよう。

```
 => ERROR [stage-0 5/5] RUN uv sync --frozen                                                                                                               0.3s
------
 > [stage-0 5/5] RUN uv sync --frozen:
0.247 error: Project virtual environment directory `/app/.venv` cannot be used because it is not a valid Python environment (no Python executable was found)
------
Dockerfile:9
--------------------
   7 |     WORKDIR /app
   8 |
   9 | >>> RUN uv sync --frozen
  10 |
  11 |     CMD ["uv", "run", "streamlit", "run", "main.py"]
--------------------
ERROR: failed to solve: process "/bin/sh -c uv sync --frozen" did not complete successfully: exit code: 2
```

:::

## Docker イメージのビルド

Docker イメージをビルドする。
今回はわかりやすい様に、タグ (`-t`) を `streamlit-uv` として打った。

```powershell
docker build . -t streamlit-uv
```

## 実際に動かしてみる

Streamlit はデフォルトポートは、`8501` となっているので、必要に応じてポートフォワーディングする。Ex) `-p 80:8501`
起動するコンテナ名 (`--name`) は `st-uv` として名付けて起動。 

```powershell
docker run -p 80:8501 --name "st-uv" streamlit-uv
```

実行すると、次の様に Docker コンテナが動作する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/1bdf04e7-569e-06e5-3d78-5fc15f8dd42e.png)

`localhost` や `localhost:80` 等でアクセスする事で、次の様にアクセスできる事が確認できる。


![2024-12-12 01.44.01 localhost b257a1ecd2f4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/513c30e6-5046-d16d-02ee-0565b1bcda85.png)


## ついでに Docker Compose も用意

実際に環境をデプロイする場合は Docker Compose もまとめて用意する事も多い。
さらには、開発環境もデプロイ環境の Docker 内を直接書き換えながら開発したいと思うので、次のように項目を追加しておく。

```yaml:compose.yaml
services:
  example:
    build: .
    ports:
      - 80:8501 # もしくはデフォルトのままであれば 8501:8501
```

### 開発環境の Docker Compose 設定も追加

```yaml:compose.yaml    
# 省略
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app
          ignore:
            - .venv/

        - action: rebuild
          path: ./pyproject.toml
```

すると、

```powershell
docker compose watch
```

コマンドを実行する事で、ローカル環境のファイルの変更を同期しながら開発できる。



# さいごに

意外と、uv ってどう入れるんやろうな？uv 周りとか自分で Docker イメージ作る時の配慮とか必要なのかな？という疑問だけがあったが、それもこうして一通りやってみたら、めちゃくちゃちゃんとサポートされてる！という気付きがあった。

とりあえず、uv で管理しているプロジェクトも、問題なくデプロイ環境への導入はいけそうだ。
動作も警戒なので、今後も更新を注視したい。