- まずゴールを決めよう:「本番でそのまま動かせる、軽くて、安全で、再現性の高いイメージ」
- ベースイメージの選び方:とりあえず公式の slim 系から始める
- 依存関係のインストール:COPY の順番とキャッシュを意識する
- 実行ユーザーと環境変数:root のまま動かさない、設定はコードに埋めない
- マルチステージビルド:ビルド用と本番用を分けて、イメージを軽くする
- ログとシグナル:コンテナらしい動かし方を意識する
- 開発用と本番用を分ける:Dockerfile を増やすか、引数で切り替えるか
- 初心者向け「これだけは守る Dockerfile ベストプラクティス」
- まとめ(Dockerfile ベストプラクティスは「軽く・安全で・再現性の高いコンテナを作るための“書き方の型”」)
まずゴールを決めよう:「本番でそのまま動かせる、軽くて、安全で、再現性の高いイメージ」
Dockerfile のベストプラクティスは、一言でいうと
「どこでビルドしても、同じように動く、無駄が少なくて、安全なコンテナを作る書き方」です。
Python だと特に大事なのは次のあたりです。
ベースイメージの選び方
依存関係のインストールの仕方
キャッシュを効かせる COPY / RUN の順番
本番用と開発用の違い(不要なものを入れない)
環境変数やユーザー権限の扱い
これを、具体例を交えながら、かみ砕いて話していきます。
ベースイメージの選び方:とりあえず公式の slim 系から始める
なぜ「python:3.12-slim」がよく出てくるのか
Python の Dockerfile でよく見るのがこれです。
FROM python:3.12-slim
Dockerfile理由はシンプルで、
公式イメージなので安心
フルイメージより軽い(不要なツールが少ない)
でも最低限の Linux 環境はある
というバランスが良いからです。
いきなり alpine に行くのは、初心者には少しハードルが高いです。
ビルドに必要なライブラリが足りなかったり、musl libc 由来のハマりポイントが出てきたりします。
まずは python:3.12-slim のような「公式の slim 系」で慣れるのが、現実的で安全なスタートです。
バージョンは「明示的に固定」する
もう一つ大事なのは、「タグをちゃんと固定する」ことです。
悪い例はこれです。
FROM python:3
Dockerfileこれだと、「いつの間にか 3.11 から 3.12 に変わっていた」みたいなことが起きます。
CI で突然テストが落ちたり、本番だけ挙動が変わったりして、地味に事故の元です。
できるだけ、
FROM python:3.12-slim
Dockerfileのように、「メジャー+マイナー」まで固定しておくのがベストプラクティスです。
依存関係のインストール:COPY の順番とキャッシュを意識する
なぜ「先に依存ファイルだけ COPY する」のか
よく見るパターンはこうです。
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install -U pip && pip install poetry && poetry install --no-dev
COPY app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfileポイントは、「依存ファイル(pyproject.toml / poetry.lock)だけ先に COPY している」ことです。
Docker のビルドはレイヤーキャッシュを使います。
COPY pyproject.toml poetry.lock ./
RUN poetry install
この二つは、「依存ファイルが変わらない限り、キャッシュが効く」ようになります。
つまり、アプリのコード(app/)をちょこちょこ変えても、毎回依存インストールをやり直さなくて済むわけです。
もし最初から全部 COPY してしまうと、
COPY . .
RUN poetry install
Dockerfileコードを1行変えるたびに、依存インストールがやり直しになります。
ビルド時間が無駄に伸びて、開発体験が悪くなります。
「依存ファイルだけ先に COPY → install → アプリ本体を COPY」
これは Dockerfile の超重要パターンです。
本番イメージには「本番に必要な依存だけ」を入れる
Poetry や pip の extras を使っている場合、
本番イメージには dev 依存を入れないのがベストです。
例えば、
RUN poetry install --no-dev
Dockerfileとすることで、pytest や開発用ツールを本番イメージから省けます。
イメージが軽くなる
攻撃面が減る(余計なツールが入っていない)
というメリットがあります。
実行ユーザーと環境変数:root のまま動かさない、設定はコードに埋めない
root ではなく「非特権ユーザー」で動かす
デフォルトの Docker コンテナは root ユーザーで動きます。
でも、本番運用では root のままアプリを動かすのは避けるのがベストプラクティスです。
Dockerfile でユーザーを作って切り替えます。
FROM python:3.12-slim
RUN useradd -m appuser
WORKDIR /app
USER appuser
# ここから先のファイルは appuser の権限で扱われる
Dockerfileこれで、コンテナ内でアプリが乗っ取られても、
root 権限で好き放題されるリスクを減らせます。
「コンテナだから安全」というわけではなく、
コンテナの中でも「最小権限の原則」を守るのがベストプラクティスです。
設定値は環境変数で渡す(Dockerfile にベタ書きしない)
よくあるアンチパターンがこれです。
ENV DATABASE_URL=postgres://user:pass@db:5432/app
ENV SECRET_KEY=super-secret
Dockerfileこれは絶対にやめた方がいいです。
Dockerfile はリポジトリに入ります。
つまり、秘密情報が Git に乗ることになります。
ベストプラクティスは、
Dockerfile では「キーの名前」だけ決める
値は docker compose やデプロイ環境(GitHub Actions の secrets など)から渡す
という形です。
ENV APP_ENV=production
Dockerfileのような「秘密ではない設定」は書いても構いませんが、
パスワードやトークンは必ず外から注入します。
マルチステージビルド:ビルド用と本番用を分けて、イメージを軽くする
「ビルドにだけ必要なもの」を本番から消す
Python でも、C拡張を含むライブラリを使うときなどは、
ビルドにコンパイラやヘッダファイルが必要になります。
その場合、マルチステージビルドが効きます。
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN apt-get update && apt-get install -y build-essential
RUN pip install -U pip && pip install poetry && poetry install --no-dev
COPY app ./app
RUN pytest # ここでテストしてもよい
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /app /app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfileここでやっていることは、
builder ステージでビルドに必要なもの(コンパイラなど)を入れる
runtime ステージには、ビルド済みの成果物だけをコピーする
という分離です。
これにより、
本番イメージからコンパイラやビルドツールを消せる
イメージサイズが小さくなる
攻撃面が減る
というメリットがあります。
最初からマルチステージを完璧に使いこなす必要はありませんが、
「ビルド用と本番用を分けられる」という発想は、覚えておくと一気にレベルが上がります。
ログとシグナル:コンテナらしい動かし方を意識する
ログはファイルではなく「標準出力」に出す
Docker コンテナでは、ログは基本的に標準出力(stdout)に出します。
アプリ側でファイルに書こうとすると、コンテナのライフサイクルとズレて扱いづらくなります。
FastAPI / Uvicorn なら、デフォルトで標準出力にログが出ます。
Python の logging も、ハンドラを標準出力に向けておくのがベストです。
コンテナオーケストレーション(Docker / Kubernetes / ECS など)は、
標準出力のログを集約してくれる前提で設計されています。
PID 1 とシグナルの扱い(プロセスを正しく終了させる)
Docker コンテナの中で動くプロセスは、PID 1 になります。
PID 1 はシグナルの扱いが少し特殊で、
正しく SIGTERM を受け取って終了できるようにしておく必要があります。
Uvicorn や Gunicorn は、基本的に SIGTERM を受け取ってきれいに終了してくれます。
Dockerfile の CMD で、シェル経由ではなく「exec 形式」で書くのがベストです。
悪い例:
CMD uvicorn app.main:app --host 0.0.0.0 --port 8000
Dockerfile良い例:
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile後者は、Uvicorn が PID 1 になり、シグナルを正しく受け取れます。
開発用と本番用を分ける:Dockerfile を増やすか、引数で切り替えるか
開発用は「便利さ」、本番用は「安全性と軽さ」
開発中は、こんなものが欲しくなります。
ホットリロード(--reload)
デバッガ
テストツール一式
本番では、これらは不要ですし、むしろ邪魔です。
ベストプラクティスとしては、
本番用 Dockerfile(Dockerfile)
開発用 Dockerfile(Dockerfile.dev)
を分けるか、ARG でモードを切り替えるか、のどちらかです。
例えば、開発用はこう。
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -U pip && pip install -e .[dev]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
Dockerfile本番用は、さっきまで話してきたように、
依存だけ先に COPY
dev 依存は入れない--reload は使わない
という形にします。
「開発の便利さ」と「本番の堅牢さ」は、Dockerfile のレベルで分けてしまう方が、結果的にシンプルです。
初心者向け「これだけは守る Dockerfile ベストプラクティス」
ここまでかなり話したので、最初に意識してほしいポイントだけ、ぎゅっと絞ります。
ベースイメージは python:3.12-slim のように公式 slim 系+バージョン固定にする
依存ファイル(pyproject.toml / requirements.txt)だけ先に COPY してから install する
本番イメージには dev 依存を入れない
秘密情報(DB パスワードなど)は Dockerfile に書かず、環境変数で外から渡す
CMD は exec 形式(配列形式)で書く
可能なら非 root ユーザーで動かす
このあたりを守るだけで、「とりあえず動く Dockerfile」から「本番に耐えうる Dockerfile」に一段階進めます。
まとめ(Dockerfile ベストプラクティスは「軽く・安全で・再現性の高いコンテナを作るための“書き方の型”」)
初心者目線で整理すると、Python の Dockerfile ベストプラクティスはこういうものです。
公式の slim 系イメージをベースにし、依存ファイルだけ先に COPY してキャッシュを効かせ、本番イメージには本番に必要なものだけを入れ、設定は環境変数で外から渡し、できれば非 root ユーザーで動かす。
マルチステージビルドや exec 形式 CMD、ログの標準出力出力などを組み合わせることで、「どこでビルドしても同じように動く、軽くて安全なコンテナ」が手に入る。
