Remote Containers + Poetryの俺最強テンプレート
tl;dr
Remote Containers + Poetryの俺最強テンプレートを作りました。
対象読者
以下の基本的な使い方がわかる人。
- VS Code Remote Remote Container
- Poetry
- Dockerfile
- Docker Compose
あるいはこれらの技術を理解したい人。(この記事を駆け抜けると、これらの知識が得られます)
目指すこと
- ローカルの開発環境を汚さない
- Remote Containersを意識せずに、アプリを起動することができる
- 開発用のコンテナの起動をできるだけ早くする
- moduleとしてpythonスクリプトを実行する
ファイル構成
最小限のファイル構成は以下になります。
.
├── .devcontainer
│ ├── devcontainer.json
│ └── docker-compose.yml
├── app
│ └── Dockerfile
└── docker-compose.yml
個別ファイルの解説
.devcontaier/devcontainer.json
VS Code Remote - Remote Containersの設定ファイルです。読み込むdocker-compose.yml
ファイルの順序と、起動するサービスを指定します。リファレンスは以下にあります。
{
"name": "poetry",
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
"service": "app",
"workspaceFolder": "/workspace",
// 省略
}
Remote Containersのコマンド(Ctrl-shift-pで選択)で、Add Development Container Configuration File
を実行した結果から作成します。
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
によって、<project root>/docker-compose.yml
を先に読み込み、.devcontaier/docker-compose.yml
の値で上書きします。このとき、docker compose
が実行されるディレクトリは、<project root>
となります。したがって、.devcoontaier/docker-compose.yml
における相対パスの記述は、<project root>
を基準にする必要があります。
"service"
には、適当なサービス名を入れておきます。
<project root>/docker-compose.yml
Remote Containersに関係なく使用するdocker-compose.yml
ファイルです。
version: "3.9"
services:
app:
container_name: app
build:
args:
- PYTHON_VERSION=3.10
context: app
dockerfile: Dockerfile
target: run
-
app:
のように、devcontainer.json
で指定したサービス名を記載します。 -
build:
以下では、コンテナイメージのビルドに必要な情報を記載します。-
PYTHON_VERSION=3.10
として、Pythonのバージョンをビルド時に渡せるようにします。 -
Dockerfile
がapp
ディレクトリ以下にあるので、context: app
としています。 -
target: run
で、実行時に用いるステージを指定します。
-
「ステージ❓」となった方は以下を参照してください。開発に用いるイメージと実行に用いるイメージの定義を同じDockerfileに記載するために、マルチステージビルドを採用しています。
リファレンスも見ておきましょう
.devcoontaier/docker-compose.yml
Remote Containersを使用する際に、プロジェクトルートのdocker-compose.yml
の定義を上書きするためのファイルです。<project root>/docker-compose.yml
にキーが存在する値はこのファイルの値で上書きされて、存在しない値は単に追加されます。
version: "3.9"
services:
app:
container_name: app-dev
build:
args:
- PYTHON_BASE_IMAGE=mcr.microsoft.com/vscode/devcontainers/python
target: ${TARGET_STAGE:-dev}
command: sleep infinity
volumes:
- .:/workspace:cached
-
app:
のように、devcontainer.json
で指定したサービス名を記載します。 -
container_name: app-dev
のように、実行時のコンテナ名と開発時のコンテナ名を分けておく方が良いです。これらの名前が衝突すると、実行方法をかえるたびにいちいちコンテナを削除する必要があります。 -
build:
以下では、開発時に用いるコンテナイメージのビルドに必要な情報を記載します。-
PYTHON_IMAGE_BASE=mcr.microsoft.com/vscode/devcontainers/python
として、開発時に用いるベースイメージを指定します。 -
target: ${TARGET_STAGE:-dev}
では、環境変数TARGET_STAGE
をビルド対象のステージに設定します。この環境変数が設定されていない場合には、デフォルトでdevステージが実行されます。
-
-
command: sleep infinity
として、コンテナが終了しないようにします。 -
volumes
の定義は、Remote Containersが生成するファイルと同じです。好みに応じて、./app
としてもよいでしょう。前述したとおり、プロジェクトルートからの相対パスで記載する必要があります。
PYTHON_IMAGE_BASE=mcr.microsoft.com/vscode/devcontainers/python
は、microsoftが提供しているRemote Containerのためのベースイメージです。最近は、以下のようにVS codeとは関係なく、コンテナ定義が提供されています。
python向けの定義は、こちらにあります。
通常のコンテナイメージ、例えばubuntu:latest
等を使うこともできるはずですが、こんな感じで バグにはまることもあるので、できるだけmicrosoftが提供しているものを利用したほうがよいでしょう。
app/Dockerfile
コンテナの定義です。ここからは、ステージごとに説明します。なお、BuildKitを前提としています。
事前定義
ステージを定義する前に、イメージのビルドに必要な引数を定義しておきます。
# syntax=docker/dockerfile:1
ARG APP_NAME="app" # アプリ名
ARG PYTHON_VERSION='3.10' # pythonのバージョン
ARG PYTHON_BASE_IMAGE='python' # 実行時のベースイメージ
ARG POETRY_CACHE_DIR='/root/.cache/poetry' # poetryのキャッシュディレクトリ
poetry ステージ
このステージでは、Poetryのインストールを行います。プロジェクトを初期化する前には、pyproject.toml
やpoetry.lock
が存在しないため、ファイルの非存在により後述するdevステージのビルドができません。そこで、ただPoetryを使えるようにするだけのステージを定義します。
FROM ${PYTHON_BASE_IMAGE}:${PYTHON_VERSION} AS poetry
ENV PYTHONUNBUFFERED=true
RUN \
\
apt-get update \
&& apt-get install -y --no-install-recommends build-essential
RUN \
\
pip install -U pip poetry
-
FROM ${PYTHON_BASE_IMAGE}:${PYTHON_VERSION} AS poetry
にて、ベースイメージとPythonのバージョンを切り替えられるようにします。 -
apt-get
に関しては、ビルド間でキャッシュがきくように設定しています。BuildKitが使える場合、イメージサイズを小さくするために、もはやapt-get clean && rm -r /var/lib/apt/lists/*
は不要です。- aptでライブラリのインストールが必要なライブラリを利用する場合には、ここでインストールしておきます。
- 次に、pipでPoetryをインストールします。
apt-get
と同様に、ビルド間でキャッシュが効くように設定しています。公式では、インストールスクリプトによるインストールが推奨されていますが、pipを使うとPATHを通すめんどくささが少しだけ減ります。
dev ステージ
このステージでは、pyproject.toml
とpoetry.lock
を用いて、依存関係のインストールを行います。
FROM poetry AS dev
ARG APP_NAME
ARG POETRY_CACHE_DIR
WORKDIR /workspace/${APP_NAME}
COPY pyproject.toml poetry.lock ./
RUN \
\
poetry config cache-dir ${POETRY_CACHE_DIR} && \
poetry config installer.parallel false && \
poetry config virtualenvs.create false && \
poetry install --no-interaction
3点、poetryのconfigを設定しています。
-
cache-dir ${POETRY_CACHE_DIR}
- poetryのキャッシュディレクトリの設定です。
-
cache-dir installer.parallel false
- パッケージのインストール時の並列実行を無効にします。筆者の環境では、この設定値をfalseにしないとインストールが失敗するという事象に悩まされました。
-
cache-dir virtualenvs.create false
- パッケージのインストール時に仮想環境を作成しないようにします。コンテナ内で開発するので、プロジェクト間でパッケージのバージョン衝突を考慮する必要がありませんし、poetryで作成した仮想環境をVS Codeに認識させるのに一手間必要になるからです。
その他のconfigのリファレンスは、こちらにあります。
最後に、poetry install --no-interaction
にてパッケージのインストールを行います。
run ステージ
このステージでは、作成したモジュールを実行するための設定をしています。ここでは、仮想環境を使用しています。VS Codeに仮想環境を認識させる必要がないからです。
FROM poetry AS run
ARG POETRY_CACHE_DIR
WORKDIR /run
COPY pyproject.toml poetry.lock ./
RUN \
\
poetry config virtualenvs.in-project true && \
poetry install --no-interaction
COPY . /run/${APP_NAME}
WORKDIR /run/${APP_NAME}
ENV APP_NAME=${APP_NAME}
ENTRYPOINT python -m ${APP_NAME}
COPYから先は、もう少し素直に書けないものだろうか・・・と思います。
Compose ファイルのテスト
さて、ファイルも準備したし、早速コンテナで開発!としたいところですが、このままReopen in Container
とすると、おそらく下記のようなわけのわからないエラーが出力されることになるでしょう。(この場合は簡単で、pyproject.toml
がないよ〜ということですが…)
failed to solve: rpc error: code = Unknown desc = failed to compute cache key: "/pyproject.toml" not found: not found
[10308 ms] Error: Command failed: docker compose --project-name app -f /Users/utt/work/app/docker-compose.yml -f /Users/utt/work/app/.devcontainer/docker-compose.yml build
[10309 ms] at Mf (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:222:419)
[10309 ms] at process.processTicksAndRejections (node:internal/process/task_queues:96:5)
[10310 ms] at async pF (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:222:2358)
[10310 ms] at async dF (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:206:2361)
[10310 ms] at async DF (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:263:2177)
[10310 ms] at async to (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:263:3110)
[10310 ms] at async Ak (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:383:8108)
[10311 ms] at async Ok (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:383:7864)
[10319 ms] Exit code 1
正直なところ、うんざり😩です。コンテナのビルドもなんだか時間がかかります。そんな時には以下のように素のdocker composeを起動してみましょう。
utt@MacBook-Pro-2 app % docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml build
[+] Building 1.3s (13/15)
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 32B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1 0.8s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9ba7531bd80fb0a858632727cf7a112fbfd19b17e94c4e84ced81e24ef1a0dbc 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load metadata for mcr.microsoft.com/vscode/devcontainers/python:3.10 0.2s
=> [internal] load build context 0.0s
=> => transferring context: 2B 0.0s
=> CANCELED [poetry 1/3] FROM mcr.microsoft.com/vscode/devcontainers/python:3.10@sha256:77225b9dc27e3585e684d695d27d62d22b93c2a7e258c0f2adac5fbb7f3f0cd1 0.0s
=> => resolve mcr.microsoft.com/vscode/devcontainers/python:3.10@sha256:77225b9dc27e3585e684d695d27d62d22b93c2a7e258c0f2adac5fbb7f3f0cd1 0.0s
=> => sha256:c5ef33b70f5dbcbc486d3dff023d6d5c27a69082544ca8b5556865b65ad8c51f 27.45kB / 27.45kB 0.0s
=> => sha256:77225b9dc27e3585e684d695d27d62d22b93c2a7e258c0f2adac5fbb7f3f0cd1 743B / 743B 0.0s
=> => sha256:910c979b5d0b9efff5e542e3b2871892f084aa27e8a64d2b99c1992d2a8fec66 5.34kB / 5.34kB 0.0s
=> CACHED [poetry 2/3] RUN --mount=type=cache,target=/var/lib/apt/lists --mount=type=cache,target=/var/cache/apt/archives apt-get update && apt-get install -y --no-inst 0.0s
=> CACHED [poetry 3/3] RUN --mount=type=cache,target=/root/.cache/pip pip install -U pip poetry 0.0s
=> CACHED [dev 1/4] WORKDIR /workspace/app 0.0s
=> ERROR [dev 2/4] COPY pyproject.toml poetry.lock ./ 0.0s
------
> [dev 2/4] COPY pyproject.toml poetry.lock ./:
------
failed to solve: rpc error: code = Unknown desc = failed to compute cache key: "/pyproject.toml" not found: not found
素のdocker composeを起動すると、VS Code Remoteの出力に紛れず、とても明確なエラーメッセージが得られます。
poetryによるプロジェクトの作成
pyproject.toml
とpoetry.lock
がなかったので、作成します。
まず、poetry ステージをビルドしてデーモンとして起動します。
TARGET_STAGE=poetry docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml up -d --build
起動に成功したら、コンテナの中に入ります。
TARGET_STAGE=poetry docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml exec app bash
/workspace
にプロジェクトディレクトリがマウントされているので、移動します。
cd /workspace/app
ここで、poetry init
とすると、色々聞かれるので、適当に答えていきます。
root ➜ /workspace/app $ poetry init
This command will guide you through creating your pyproject.toml config.
Package name [app]: app
Version [0.1.0]:
Description []:
Author [None, n to skip]: None
License []:
Compatible Python versions [^3.10]:
Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file
[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["None"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Do you confirm generation? (yes/no) [yes] yes
パッケージを追加したい場合には、poetry add <package_name>
です。fastapi
を入れてみます。
root ➜ /workspace/app $ poetry add fastapi
Using version ^0.85.0 for fastapi
Updating dependencies
Resolving dependencies... (1.4s)
Writing lock file
Package operations: 7 installs, 0 updates, 0 removals
• Installing idna (3.4)
• Installing sniffio (1.3.0)
• Installing anyio (3.6.1)
• Installing typing-extensions (4.3.0)
• Installing pydantic (1.10.2)
• Installing starlette (0.20.4)
• Installing fastapi (0.85.0)
これで、pyproject.toml
とpoetry.lock
が生成されたので、コンテナから出ます。
root ➜ /workspace/app $ exit
exit
作成したコンテナは削除しておきましょう。
TARGET_STAGE=poetry docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml down
VS Codeでの開発
これで、VS Code上でPythonの開発ができるようになっています。
[Ctrl]+[Shift]+[p]のコマンドから、Remote-Containers: Rebuild Container without Cache
を選択すると、無事起動するはずです。
Pythonプロジェクトの構成
Python プロジェクトの構成は、筆者は以下のようにしています。
├── app
│ ├── Dockerfile
│ ├── app
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── cli.py
│ │ └── src
│ │ └── __init__.py
│ ├── poetry.lock
│ └── pyproject.toml
パッケージの追加
devステージで直接poetry add
を実行しようとすると、permission deniedのエラーが出ることがあります。
この時にはsudo su
として、rootユーザになると、poetry add
を実行することができます。
VS Codeなしでの起動
これだけです!気持ちいいですね。
docker compose up --build
おわりに
初めての記事でした。書くのは大変です。でも、かなり応用の効く内容になっていると思います。先人に感謝。
Discussion