🍞sales_data_appのDockerfileを実際に書き換えて、Before/Afterを計測したら分かったこと
はじめに
現役のトラックドライバーとして7年以上現場を走りながら、Pythonを独学しているtosane932です。(学習時間は現在*125時間)
これまで「積載ミス」や「防御的プログラミング×停止距離」など、物流の現場経験をベースにした技術記事を書いてきました。今回は自作アプリ「sales_data_app」のDockerfileを実際に書き換えて、マルチステージビルドを導入する前と後で、イメージサイズが本当に変わるのかを計測してみた記録です。
先に結論を書いておくと、期待していたほど劇的には変わりませんでした。ただ、その「なぜ減らなかったのか」を調べる過程で得られた学びの方が、今回は収穫が大きかったです。
🐋今までのDockerfile
sales_data_appは Flask + PostgreSQL + Gemini API という構成のアプリです。今までのDockerfileはこんな感じでした。
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
シンプルですが、ビルドに必要な道具(コンパイラなど)まで、本番で動かす環境にそのまま残ってしまう構成です。
これを物流に例えると、 「工場の重い工具箱や梱包資材のゴミまで、本番の高速道路を走るトラックに積みっぱなしにしている」 状態に近いと思っています。荷物を作るための道具は、荷造り場に置いてくればいいのに、トラックにそのまま載せて走っているイメージです。
📦マルチステージビルドという「積み替え」
そこで導入したのが、マルチステージビルドです。
# ステージ1: 荷造り場(builder)
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# ステージ2: 本番トラック
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]
考え方はシンプルです。
-
ステージ1(荷造り場):パッケージのビルドに必要な道具(
gcc、libpq-dev)を用意して、荷造り(pip install)を終わらせる -
ステージ2(本番トラック):荷造り場から完成した荷物だけ(
/root/.local以下)を積み替えて、本番の環境には道具箱(gcc一式)を持ち込まない
こうすることで、本番用のイメージには「動かすために本当に必要なもの」だけが載る、という設計です。
📊実際に計測してみた
docker images コマンドで、Before/Afterのサイズを比較しました。
| CONTENT SIZE | |
|---|---|
| Before(シングルステージ) | 212MB |
| After(マルチステージ) | 209MB |
削減できたのは、わずか 3MB でした。
正直、記事のタイトルに「劇的に軽量化した」と書きたい気持ちもありましたが、事実ベースで書くと決めているので、そのまま報告します。
❓なぜ劇的に減らなかったのか
原因を調べてみると、requirements.txt の中身に理由がありました。
sales_data_appが使っている psycopg2-binary は、その名の通りあらかじめコンパイル済みのバイナリ(wheel形式)として配布されているパッケージです。つまり、ビルド時にgccのようなコンパイラを用意しなくても、コンパイル済みのファイルをダウンロードしてくるだけで動いてしまいます。
今回のケースでは、そもそも「重い工具箱(gcc、libpq-dev)」を用意すること自体、あまり意味がなかった可能性が高いです。マルチステージビルドの効果がはっきり出るのは、psycopg2(binaryではない方)のようにその場でソースコードからコンパイルする必要があるパッケージを使っている場合だと考えられます。
マルチステージビルドは「魔法のように必ずサイズが減る技術」ではなく、「依存パッケージの性質によって効果が変わる技術」 という、実際にやってみないと分からなかった学びでした。
🔐副産物:API keyの扱い方を見直すきっかけになった
書き換えたイメージを起動したところ、Gemini APIキーが認識されず、エラーになりました。
[ERROR] GEMINI_API_KEY is missing from environment variables.
原因は単純で、ホスト側の .env ファイルは自動的にコンテナに渡らない、という基本的な仕様でした。
docker run -p 5000:5000 --env-file .env sales-after
これで解決したのですが、この一件で改めて意識したのが、「イメージ本体」と「秘密の鍵(APIキー)」は別物として扱うべきという考え方です。
Dockerイメージは、GitHubなどと同じように共有・配布されうるものです。もしAPIキーをイメージの中に焼き込んでしまうと、
- イメージを誰かに渡した瞬間、APIキーも一緒に渡ってしまう
-
docker historyなどでレイヤーの中身を覗かれる可能性がある
といったリスクがあります。これは、GitHubに.envを含めないよう.gitignoreで除外するのと、根っこは全く同じ考え方です。「コードや入れ物」と「秘密の鍵」を分離するという原則を、Dockerでも改めて実践する形になりました。
まとめ
- マルチステージビルドを導入したが、今回のケースではイメージサイズの削減は3MBにとどまった
- 理由は、使用しているパッケージ(psycopg2-binary)がすでにコンパイル済みだったため
- マルチステージビルドの効果は、依存パッケージの性質に左右される
- 副産物として、APIキーとイメージを分離して扱う重要性を再確認できた
次回は、今回の作業と並行して導入を進めているpytestとCI/CDについて、実際につまずいたポイントを交えて書く予定です。
※(この記事は実際の計測結果に基づいて書いています。環境やパッケージ構成によって結果は変わる可能性があります。)
GitHubはこちらをご覧ください👇
