Poetry+DockerでのPython環境構築（2023年版）


私が普段使っているPythonの開発環境の構築について説明します。[Docker](https://www.docker.com/)と[Poetry](https://python-poetry.org/)を利用して構築し、1人または少人数で利用することを想定した開発環境です。

本稿では、はじめに最低限の構成でJupyterLabを起動する方法までを説明します。後にオプションとしてVS CodeやGitHub Codespacesの各種設定やフォーマッター、テスト、API構築などのトピックを扱います。使っているエディタなどに合わせて、必要なところだけ参考にしてください。

なぜDocker+Poetryという構成が良いのか、他の選択肢にはどういった物があるのかという点については、[Pythonの開発環境の3つの観点をおさえよう](https://zenn.dev/os1ma/articles/935f6e653f1052)という記事に詳しく説明されています。

## 実行環境
- Debian 11.7
- Docker version 24.0.6
- Docker Compose version v2.21.0

Linux（ChromeOS）およびWindows 11 WSL2を想定しています。
ホストコンピュータにPythonやPoetryをインストールする必要はありません。

## Python環境構築

### poetry new
Poetryを使う場合、最初に`poetry new`でプロジェクトを初期化する必要があります。ですがPoetryはPythonに依存しており、Python環境構築前にPoetryは使えません。
そのため、Dockerで一時的なコンテナを使って`poetry new`を実行します。

```bash
$ docker run \
  --volume $(pwd):/app \
  --workdir /app \
  python:3.10-slim bash -c \
  'pip install poetry && poetry new my_python_project && cd $_ && poetry install'
```

```bash
$ cd my_python_project
```

以下はお好みで変更してください。

- Dockerイメージ（Pythonのバージョン） `python:3.10-slim`
- プロジェクト名 `my_python_project`

:::note info
Pythonのバージョンは、2023年10月時点で最新版である3.12がリリースされていますが、どのバージョンを選択するかはいくつかの方針があります。
最新版にすると最新の機能が利用できますが、有名なライブラリ等が対応していない時もあります。そういったトラブルを避ける必要がある場合はひとつ前のバージョンを選ぶのが安定すると思います。

|方針|現時点のバージョン|
|:--|:--|
|最新版にする|3.12|
|最新版のひとつ前にする|3.11|
|Google Colabに合わせる|3.10|
:::

### Gitの設定

```bash
$ git init .
$ wget --output-document=.gitignore https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
```

お好みでlockファイルを[バイナリ扱い](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA-Git-%E3%81%AE%E5%B1%9E%E6%80%A7)にします。
賛否が分かれる設定だと思いますが、最近[BunがLockファイルをバイナリとした](https://bun.sh/docs/install/lockfile)のもあって、一定の支持はある考え方のようです。

```bash
$ echo "poetry.lock binary" >> .gitattributes # お好みで
```

### Dockerの設定
Docker Composeで起動する設定をします。

```Dockerfile
# 最初に構築したイメージと合わせる
FROM python:3.10-slim
WORKDIR /app

# お好みで好きなパッケージを追加
RUN apt-get update && apt-get install -y \
    build-essential \
    git \
    curl \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

RUN git config --global --add safe.directory /app

RUN pip install poetry \
  && poetry config virtualenvs.create false

COPY ./pyproject.toml ./poetry.lock* ./
RUN poetry install
```

```yaml:compose.yaml
services:
  app:
    build: .
    volumes:
      - .:/app
    command: sleep infinity
```

ビルドしておきます。

```bash
$ docker compose build
```

### パッケージのインストールと起動
次に、パッケージをインストールします。ここでは例としてJupyterLabをインストールし、起動設定をします。

```bash
$ docker compose run --rm app poetry add --group dev jupyterlab ipywidgets
```

`compose.yaml`に起動設定をします。JupyterLabしか起動しないコンテナなのであれば、`compose.yaml`ではなく`Dockerfile`に`CMD`で指定する方が良いと思います。

 ```diff_yaml:compose.yaml
services:
  app:
    build: .
    volumes:
      - .:/app
 -  command: sleep infinity
+  ports:
+    - 8888:8888
+  command: poetry run jupyter lab --no-browser --ip=0.0.0.0 --port=8888 --allow-root --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.disable_check_xsrf=True
```

なお[VS CodeからNotebookを使う場合](https://code.visualstudio.com/docs/datascience/jupyter-notebooks)などは、実は8888ポート開放の必要はありません。複数プロジェクトを同時に起動する必要がある場合などにポートの競合が起きなくなり便利です。

```bash
$ docker compose up --build
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/4d1a19f9-6eb6-bab7-137b-e09bd80d2fb0.png)

JupyterLabを起動できたら成功です。

:::note info
JupyterLabを使う必要がないがコンテナを起動したい場合、コマンドを`sleep infinity`などのように指定しておくことで起動状態を維持できます。
:::

## オプション
以下はオプションの設定です。お好みで必要な設定を取り入れてください。

### OpenAI API環境変数の追加
ChatGPTを利用したアプリケーションを利用したい場合のAPIキーなどは、環境変数として配置します。APIキーやChatGPTについての詳細はここでは省略します。

```bash
$ echo "OPENAI_API_KEY=XXXXXXXX" >> .env
```

```diff_yaml:compose.yaml
services:
  app:
    build: .
    volumes:
      - .:/app
+   env_file:
+     - .env
    command: sleep infinity
```

`.env`は[gitignore/Python\.gitignore](https://github.com/github/gitignore/blob/main/Python.gitignore)に含まれているため、手順通り設定している場合はリポジトリに含まれません。

### VS CodeのPython設定追加
VS Codeを使っている場合は起動中のコンテナ内にアタッチして使うことで、Pythonランタイムに沿った補完などの恩恵が得られたり、ターミナルでコンテナ内のコマンドを実行できるようになります。

VS Codeを起動したらコマンドパレットを開いて`>Dev Containers: Attatch to Running Container`コマンドなどで、Dev Containersで接続します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/8a761e9d-7a55-e45b-197d-44a8318acbc5.png)

コンテナ内でワークスペースを開いたら、`/app`（WORKDIRで設定したディレクトリ）を開きます。
コンテナ内で[ms-python.python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)と[ms-toolsai.jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter)の拡張機能をインストールします。推奨事項に追加する設定をすると、インストールされていない環境でVS Codeを開くと通知が来るようになります。

```.vscode/extensions.json
{
  "recommendations": [
    "ms-python.python",
    "ms-toolsai.jupyter"
  ]
}
```

拡張機能をインストールしたら、`>Developer: Reload Window`コマンドでリロードします。
VS Code上のNotebookでPythonを実行などができたら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/c721a19f-8b16-1cb1-d24a-4d095e89dbb6.png)

なおVS Code内からのみNotebookを使うためWebブラウザからアクセスできる必要が無いという場合、JupyterLabのための8888ポート開放の必要が無くなります。複数プロジェクトを同時に起動する必要がある場合などにポートの競合が起きなくなったりして少し便利です。

 ```diff_yaml:compose.yaml
services:
  app:
    build: .
    volumes:
      - .:/app
-  ports:
-    - 8888:8888
    command: poetry run jupyter lab --no-browser --ip=0.0.0.0 --port=8888 --allow-root --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.disable_check_xsrf=True
```

#### Dev Container
記述した`Dockerfile`や`compose.yaml`を利用して、[Development Containers（Dev Container）](https://containers.dev/)に沿って定義することもできます。

```.devcontainer/devcontainer.json
{
  "dockerComposeFile": "../compose.yaml",
  "workspaceFolder": "/app",
  "service": "app",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-toolsai.jupyter",
        "ms-python.flake8",
        "ms-python.black-formatter"
      ]
    }
  }
}
```

VS Codeを使う上では、Dev Containerを使うことでワークスペースディレクトリ`/app`を指定しなくても良くなったり、拡張機能を自動でインストールできる等のメリットがあります。`$ docker`のCLIコマンドを使わずに起動できるという点は、好みが分かれると思います。

#### GitHub Codespaces
先述のDev Containerの定義を利用して、[GitHub Codespaces](https://github.co.jp/features/codespaces)で開発環境を動作させることもできます。

その際、先述の設定ファイルでは`env_file`を用意できなくて環境が立ち上がりません。GitHubのsecretsに設定した上でファイルを作成する必要があります。

```diff_json:.devcontainer/devcontainer.json
  "initializeCommand": "if [ ! -e .env ]; then echo OPENAI_API_KEY=`echo $OPENAI_API_KEY` > .env; fi"
```

また、Codespacesのログは`>Codespaces: Export Logs`コマンドを使うとzipで固めた状態で取得できるため、コンテナ内に`unzip`をインストールするよう`Dockerfile`に追記すると便利だと思います。

#### VS CodeによるType Checking
VS CodeのPython拡張機能があれば、Pythonの型チェックを有効にできます。

```.vscode/settings.json
{
  "python.analysis.typeCheckingMode": "basic",
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/9405ea03-7205-4ae8-a2b8-dc31bb2ee458.png)

### Flake8およびBlackの導入
リンターとフォーマッターを導入します。本稿ではリンターに[Flake8](https://flake8.pycqa.org/en/latest/)、フォーマッターに[Black](https://black.readthedocs.io/en/stable/)を利用します。

#### VS Code拡張機能
VS Codeを介してのみFlake8とBlackを利用する場合は、拡張機能をインストールするのみでOKです。`$ pip install flake8`などをする必要はありません。

```diff_json:.vscode/extensions.json
 {
   "recommendations": [
     "ms-python.python",
     "ms-toolsai.jupyter",
+    "ms-python.flake8",
+    "ms-python.black-formatter",
   ]
 }
 ```

VS Codeで以下のように設定します。

```.vscode/settings.json
{
  "[python]": {
    "editor.formatOnSave": true,
    "editor.formatOnSaveMode": "modifications",
    "editor.defaultFormatter": "ms-python.black-formatter",
  },
  "black-formatter.args": ["--line-length", "120"],
  "flake8.args": ["--max-line-length", "120"],
}
```

リンターによる指摘が表示され、`>Format Document`コマンドでフォーマッターを使えるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50391/04db1a67-5f44-d769-bdfe-549109066347.png)

#### CLI
リンターやフォーマッターをCLIで利用したい場合は、インストールが必要です。

```sh
$ docker compose exec app poetry add --group dev black flake8
```

以下のように設定をします。

```setup.cfg
[flake8]
max-line-length = 120
```

```diff_toml:pyproject.toml
+
+[tool.black]
+line-length = 120
```

以下のようなコマンドで実行します。

```sh
$ docker compose exec app poetry run flake8 ./my_python_project/
Skipping virtualenv creation, as specified in config file.
./my_python_project/nicegui_main.py:4:1: E302 expected 2 blank lines, found 1
```

```sh
$ docker compose exec app poetry run black ./my_python_project/
Skipping virtualenv creation, as specified in config file.
reformatted /app/my_python_project/nicegui_main.py

All done! ✨ 🍰 ✨
1 file reformatted, 1 file left unchanged.
```

実際にはGitHub ActionsなどのCI上で実行するように設定するのが良いと思います。

### アプリケーション実行環境の追加
#### FastAPIの追加
Pythonで開発したものをWeb APIとして実行できる環境も用意したい場合があります。本稿では[FastAPI](https://fastapi.tiangolo.com/ja/)を起動できる環境を用意します。

```bash
$ docker compose exec app poetry add fastapi uvicorn
```

以下のように記載します。

```diff_yaml:compose.yaml
services:
  app:
    build: .
    volumes:
      - .:/app
    command: poetry run jupyter lab --no-browser --ip=0.0.0.0 --port=8888 --allow-root --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.disable_check_xsrf=True
+  fastapi:
+    extends:
+      service: app
+    ports:
+      - 8081:80
+    command: uvicorn my_python_project.fastapi_main:app --host 0.0.0.0 --port 80 --reload
```

FastAPI以外を使う場合もだいたい似たような設定で行けると思います。起動ホストを`0.0.0.0`で指定するのがポイントです。

```python:my_python_project/fastapi_main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}
```

コンテナを立ち上げ直し、`$ curl localhost:8081`などで`{"Hello": "World"}`と表示されたら成功です。

#### NiceGUIの追加
WebAPIだけではなくUIが欲しい場合もあります。要件や好みによって[Streamlit](https://streamlit.io/)や[Gradio](https://www.gradio.app/)を選びますが、本稿ではより柔軟にアプリケーションを構築できる[NiceGUI](https://nicegui.io/)の例を記載します。

```bash
$ docker compose exec app poetry add nicegui
```

以下のように記載します。

```diff_yaml:compose.yaml
services:
  app:
    build: .
    volumes:
      - .:/app
    command: poetry run jupyter lab --no-browser --ip=0.0.0.0 --port=8888 --allow-root --NotebookApp.token='' --NotebookApp.password='' --NotebookApp.disable_check_xsrf=True
+  nicegui:
+    extends:
+      service: app
+    ports:
+      - 8080:8080
+    command: poetry run python my_python_project/nicegui_main.py
```

```my_python_project/nicegui_main.py
from nicegui import ui

ui.label('Hello NiceGUI!')

ui.run()
```

### pytest
開発が進むとテストコードが欲しくなります。ここでは[pytest](https://docs.pytest.org/en/7.4.x/)を導入します。

```
$ docker compose exec app poetry add --group dev pytest
```

`tests/test_mymodule.py`のような名前でテストファイルを作成しますが、そのままではテストを実行できません。`pyproject.toml`に、pytestの設定を記述する必要があります。

```diff_toml:pyproject.toml
+
+[tool.pytest.ini_options]
+testpaths = ["tests",]
+pythonpath = "my_python_project"
```

`$ poetry run pytest`でテストが実行できます。

## 補足
本記事は[3年前に書いた記事のリメイク](https://qiita.com/suzuki_sh/items/6bc15446965df20b6c5a)です。