とっても軽いDistrolessコンテナでPythonとFlaskを動かす
私はPSHというActivityPub実装サーバーをVercel上で動かしたりしているのですが、Vercelで動かすのに飽きたので最近はRaspberry Pi ZeroやCGIに飽き足らずDockerやらDocker Composeやらで動かそうとしています。
そしてとっても軽いDistrolessコンテナのPython用のイメージが前から実験的なもので公開されていたことを思い出し、見に行ったらgcr.io/distroless/python3-debian12
が実験的なものではなかったため、本番環境で動かしてもよさそうだったので今回採用することにしました。
よくわかりませんが、以前からあったgcr.io/distroless/python3-debian11
は変わらずまだ本番環境で使えない状態でした。
まあbookwormのほうが新しいのでこのgcr.io/distroless/python3-debian12
を使うことにします。
$ touch Dockerfile app.py
まずは素振りから始めてみることにします。ふたつファイルを作って、Dockerfileを書きます。
FROM python:3.11-slim-bookworm as builder
WORKDIR /app
COPY . .
ENV PYTHONUSERBASE=/app/__pypackages__
RUN pip install --user Flask gunicorn
...
gcr.io/distroless/python3-debian12
はPython 3.11が動くbookwormなので、マルチステージビルドで最初にビルド用のコンテナを動かします。
slimを使えば通常のDebianより軽いものが使えるので、python:3.11-slim-bookworm
を指定しましょう。
ビルドを名乗っていますがやってることはpipでFlaskとGunicornをインストールするだけです。
debugイメージ含め、DistrolessコンテナのPython用のイメージはpipやwheelが使えず、基本pythonコマンドとpython3(python3.11)コマンドしか使えません。
debugイメージではDistrolessコンテナ共通でBusyBoxのコマンドが使えます。
pip installするとデフォルトでは/usr/local/lib/python3.11/site-packagesあたりにパッケージがインストールされるのですが、マルチステージビルドで/usr/local/**/*にあるパッケージ以外のファイルやディレクトリをコピーしたくないので、環境変数PYTHONUSERBASEを使い、WORKDIRの__pypackages__
ディレクトリにパッケージをインストールすることにしました。
...
FROM gcr.io/distroless/python3-debian12:debug
# FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY /app .
ENV PYTHONUSERBASE=/app/__pypackages__
ENTRYPOINT ["sh", "-c", "python -uB -m gunicorn app:app -b :${PORT:-8080}"]
# ENTRYPOINT ["python", "-uB", "-m", "gunicorn", "app:app", "-b", ":8080"]
続いてビルド用のコンテナからDistrolessコンテナにapp.pyとパッケージをコピーします。
先程WORKDIRに__pypackages__
を置いたおかげでCOPYコマンドが1回で済むようになりました。
また、先程と同様に環境変数PYTHONUSERBASEでパッケージインストール対象ディレクトリを指定することで、パッケージが自動的にインポートされます。
最後にENTRYPOINTでシェルを使いたいのでdebugイメージを使います。
もしポート番号決め打ちでよければBusyBoxのAshが不要になるので、コメントアウトした内容の通りpythonコマンドのみが使えるnondebugイメージでよくなります。
サイズは1MB程度の差しかありませんがセキュリティリスクが減ります。
ふたつ合体させるとこんな感じになります。
FROM python:3.11-slim-bookworm as builder
WORKDIR /app
COPY . .
ENV PYTHONUSERBASE=/app/__pypackages__
RUN pip install --user Flask gunicorn
FROM gcr.io/distroless/python3-debian12:debug
WORKDIR /app
COPY /app .
ENV PYTHONUSERBASE=/app/__pypackages__
ENTRYPOINT ["sh", "-c", "python -uB -m gunicorn app:app -b :${PORT:-8080}"]
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "<p>Hello, World!</p>"
FlaskでHello, World!を書きます。3.0の公式ドキュメントにあったものをコピペしただけです。
実際に動かしてみましょう。
$ docker build -t distroless-flask .
[+] Building 14.6s (13/13) FINISHED
...
$ docker run -d -p 8080:8080 -e PORT=8080 distroless-flask
$ curl http://localhost:8080/
<p>Hello, World!</p>
うまくいきました。よかったですね。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
distroless-flask latest ... About a minute ago 59.7MB
alpine-flask latest ... 3 minutes ago 57.5MB
大体60MBぐらいのコンテナができました。うーん軽い。
gcr.io/distroless/python3-debian12
をpython:alpine
に差し替えても2MB程度の差しかありません。
皆さんもとっても軽いDistrolessコンテナとPythonで遊んでみてはいかがでしょうか。
2024/02/23追記
当初この記事では以下のようにWORKDIRのsite-packagesディレクトリにインストールした後、環境変数PYTHONPATHでインポート対象ディレクトリのパスを指定する方法を紹介していました。
...
RUN pip install --no-cache-dir --target=site-packages Flask gunicorn
...
ENV PYTHONPATH=/app/site-packages
...
その後、環境変数PYTHONUSERBASEにパッケージインストール対象ディレクトリを指定すればパッケージが自動的にインポートされ、さらに--user
オプションを使うだけで簡単に対象ディレクトリを指定できることに気づいたので、現在の記事で紹介しているように修正しました。
またマルチステージビルドでは--no-cache-dir
がなくてもよさそうだったのでなくしており、パッケージインストール先として__pypackages__
を採用しました。
...
ENV PYTHONUSERBASE=/app/__pypackages__
RUN pip install --user Flask gunicorn
...
ENV PYTHONUSERBASE=/app/__pypackages__
...
結果、以下のメリットが生まれました。
- siteパッケージから情報を取得できる
-
--user
オプションはユーザーインストールで広く使われているため親しみやすい -
__pypackages__
ディレクトリはPEP 582でも紹介しているローカルパッケージディレクトリのため、様々なツールで使われている- pdm
- Python.gitignore
- ruff.tomlのexclude
Discussion