🐈

poetryで作るスリムなDockerイメージ

2021/02/13に公開

概要

コンテナ利用者は誰しもスリムなイメージを欲しています。

しかしながら、Pythonにおいてはパッケージ管理の仕組みが乱立している上、コンテナ上に環境を再現する方法も多岐に亘っており、なかなか良い方法が見つからなかったのでここにメモを残しておきます。

今後のPython・周辺ツールのアップデートでより良い方法が見つかるかもしれないので、その際はより良い方法に更新するかもしれません。

Dockerfile

通常、コンテナでvenvによる仮想環境を利用する意味はあまり無いため、システム環境にインストールすることが多いです。
しかし、余計なパッケージが入らないように色々と工夫したり、使用するコマンドを慎重にコピーする必要が出てきます。

ここではあえて、仮想環境にインストールを行い、仮想環境ごとプロダクションステージにコピーすることで、漏れなく・最小限の環境を構築します。

# ステージ毎のバージョンは統一したい。バージョンアップ時に毎回全て更新するのはミスが起こりかねない為、変数化。
ARG PYTHON_ENV=python:3.8.7-slim

FROM $PYTHON_ENV as build

# ビルドステージ。ライブラリが全てインストール出来れば、何をインストールしても良い。
RUN apt-get update
RUN apt-get install -y build-essential swig mecab libmecab-dev mecab-ipadic-utf8

# 最小限インストールに必要なパッケージのインストール。
# 稀にCythonが無いとインストール出来ないパッケージがあるため、その場合は先に入れておく。
RUN pip install -U pip poetry Cython

RUN mkdir -p /app
WORKDIR /app

# カレントディレクトリに.venvディレクトリを作成(virtualenvs.in-project=true)し、環境を構築。
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.in-project true && \
    poetry install --no-dev --no-interaction

# プロダクション用ステージ。最小限必要なファイルのみをインストールする
FROM $PYTHON_ENV as prod

RUN apt-get update && \
    apt-get install -y mecab mecab-ipadic-utf8 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 基本的にはこのディレクトリに、全てのライブラリとコマンドがインストールされている。
# (稀に例外あり。但しその場合はsite-packagesをコピーする方法でも動かないと思われる)
# site-packagesをコピーする方法に比べ、binを含めた仮想環境をそのまま持ってくるため、pipでインストールされたコマンド等も利用可能。
COPY --from=build /app/.venv /app/.venv
ENV PATH=/app/.venv/bin:$PATH

# 必要なファイルをコピー。ご自由に。
COPY src /app/src

WORKDIR /app/src

EXPOSE 8080

# poetryでインストールされたコマンドがそのまま利用可。
CMD ["gunicorn", "-c", "config.py", "asgi:app"]

その他更に改善する場合のポイント

pyproject.tomlのバージョンを固定

バージョンを極力1ファイルで管理しようとすると、必然的にpyproject.tomlに書くことになると思います。しかし、pyproject.tomlはビルドステージの最初で利用している為、バージョン更新の度に全依存ライブラリが再インストールされてしまいます。

ビルドステージで利用するpyproject.tomlをコンテナビルド直前にバージョン固定された物に差し替え手しまう事で、これを防ぐことが出来ます。

jqのようなコマンドラインでTOMLを書き換えるツールと、Makefile等のビルドツールを使えば容易に実現可能でしょう。

ビルドステージの共有

通常はビルドステージにはタグが付かず、レポジトリにPushされるのは最終ステージのみです。
ビルドステージからコピーするファイルが極端に大きい場合、ビルドステージも合わせて共有してしまうことで、複数人で扱う際にキャッシュが効きやすくなるかもしれません。

これには、Dockerの場合--targetオプションを利用します。--targetオプションを使うことで、各ステージを個別に実行して、タグを付与する事が出来ます。
もちろん、付与したタグでレポジトリにPushすることも可能です。

$ docker build -t build_stage_image --target build -f Dockerfile .

Discussion