🧪

とっても軽いDistrolessコンテナでPythonとFlaskを動かす

2023/11/24に公開

https://gitlab.com/tkithrta/psh

私はPSHというActivityPub実装サーバーをVercel上で動かしたりしているのですが、Vercelで動かすのに飽きたので最近はRaspberry Pi ZeroやCGIに飽き足らずDockerやらDocker Composeやらで動かそうとしています。

そしてとっても軽いDistrolessコンテナのPython用のイメージが前から実験的なもので公開されていたことを思い出し、見に行ったらgcr.io/distroless/python3-debian12が実験的なものではなかったため、本番環境で動かしてもよさそうだったので今回採用することにしました。

https://github.com/GoogleContainerTools/distroless
https://github.com/GoogleContainerTools/distroless/issues/1409

よくわかりませんが、以前からあった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 --from=builder /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程度の差しかありませんがセキュリティリスクが減ります。

ふたつ合体させるとこんな感じになります。

Dockerfile
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 --from=builder /app .

ENV PYTHONUSERBASE=/app/__pypackages__
ENTRYPOINT ["sh", "-c", "python -uB -m gunicorn app:app -b :${PORT:-8080}"]
app.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

FlaskでHello, World!を書きます。3.0の公式ドキュメントにあったものをコピペしただけです。

https://flask.palletsprojects.com/en/3.0.x/quickstart/

実際に動かしてみましょう。

$ 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-debian12python:alpineに差し替えても2MB程度の差しかありません。

皆さんもとっても軽いDistrolessコンテナとPythonで遊んでみてはいかがでしょうか。

https://zenn.dev/tkithrta/articles/7ca7ab166bbac8
https://blog.inductor.me/entry/alpine-not-recommended
https://peps.python.org/pep-0656/


2024/02/23追記

当初この記事では以下のようにWORKDIRのsite-packagesディレクトリにインストールした後、環境変数PYTHONPATHでインポート対象ディレクトリのパスを指定する方法を紹介していました。

...
RUN pip install --no-cache-dir --target=site-packages Flask gunicorn
...
ENV PYTHONPATH=/app/site-packages
...

https://note.nkmk.me/python-import-module-search-path/

その後、環境変数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

https://docs.python.org/ja/3/library/site.html
https://pip.pypa.io/en/stable/user_guide/#user-installs
https://peps.python.org/pep-0370/
https://peps.python.org/pep-0582/

Discussion