3.4 docker build 完全実践|Webアプリを一からDockerize するステップバイステップ

【第3章】Dockerfileによるイメージ構築

docker build 完全実践|Webアプリを一からDockerize するステップバイステップ

3-1〜3-4 でDockerfileの文法・レイヤーキャッシュ・マルチステージ・.dockerignore を学んできました。この記事ではそれら全てを 1つの実プロジェクトに適用し、本番で使える Dockerfile を段階的に育てていきます。

題材は Python Flask の小さなWeb API。最小構成から始めて、最適化・マルチステージ化・セキュリティ強化へと4段階で改善し、最終的にビルド時間と本番イメージサイズが両立された “プロ仕様” のDockerfileを完成させます。


目次

  1. 題材プロジェクトの構成
  2. ステップ1:最小構成(まず動かす)
  3. ステップ2:レイヤーキャッシュ最適化
  4. ステップ3:マルチステージ化
  5. ステップ4:セキュリティ強化(非rootユーザー)
  6. 4段階の比較:サイズ・ビルド時間・安全性
  7. docker build 実用コマンド集
  8. CI/CDへの繋ぎ方
  9. まとめ

1. 題材プロジェクトの構成

シンプルな Flask の JSON API を題材にします。ファイル構成は以下:

flask-api/
 ├── app.py ← アプリ本体
 ├── requirements.txt ← 依存関係
 ├── tests/ ← テストコード(本番不要)
 │ └── test_app.py
 ├── .env ← 環境変数(本番不要・要除外)
 ├── .git/ ← Git履歴(本番不要・要除外)
 ├── README.md ← ドキュメント(本番不要)
 ├── Dockerfile
 └── .dockerignore

app.py

from flask import Flask, jsonify
 
 app = Flask(__name__)
 
 @app.get("/health")
 def health():
  return jsonify(status="ok")
 
 @app.get("/hello/<name>")
 def hello(name):
  return jsonify(message=f"Hello, {name}!")
 
 if __name__ == "__main__":
  app.run(host="0.0.0.0", port=8000)

requirements.txt

flask==3.0.0
 gunicorn==21.2.0

2. ステップ1:最小構成(まず動かす)

まずは「とりあえず動く」Dockerfileから。初心者がよく書く、あえて悪い例です。

# Dockerfile (v1: 最小構成)
 FROM python:3.12
 
 WORKDIR /app
 COPY . .
 RUN pip install -r requirements.txt
 
 EXPOSE 8000
 CMD ["python", "app.py"]
# ビルド&起動
 $ docker build -t flask-api:v1 .
 $ docker run -p 8000:8000 flask-api:v1
 $ curl http://localhost:8000/hello/world
 {"message":"Hello, world!"}

何が問題か

問題 影響
ベースイメージが python:3.12(full版) イメージが大きい(約 1GB
COPY . .pip install の順 コード修正で毎回 pip install が走る
.dockerignore がない .env / .git / tests / README が本番イメージに混入
python app.py で起動 Flaskの開発サーバー(本番非推奨)
root ユーザーで実行 セキュリティリスク

これを 4段階で改善していきます。


2. ステップ2:レイヤーキャッシュ最適化

まず 3-2 で学んだキャッシュ最適化と 3-3 の .dockerignore を適用します。

.dockerignore を追加

# .dockerignore
 .git/
 .gitignore
 .env
 .env.*
 README.md
 tests/
 __pycache__/
 *.pyc
 *.pyo
 .pytest_cache/
 .venv/
 venv/
 .vscode/
 .idea/
 Dockerfile
 .dockerignore

Dockerfile v2

# Dockerfile (v2: キャッシュ最適化)
 FROM python:3.12-slim
 
 WORKDIR /app
 
 # ① 依存定義だけ先にコピー
 COPY requirements.txt .
 
 # ② 依存インストール(キャッシュの主役)
 RUN pip install --no-cache-dir -r requirements.txt
 
 # ③ アプリコードは最後
 COPY . .
 
 EXPOSE 8000
 # 本番向けに gunicorn に変更
 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

改善点

  • ベースを python:3.12-slim に変更(1GB → 約 150MB
  • COPY分割で、コード修正時に pip install をスキップ
  • .dockerignore で不要ファイル除外
  • 起動コマンドを本番向けの gunicorn に変更
💡 python:slim を選ぶ理由
python:3.12(full版)は Debian full ベースで開発ツールが多く含まれます。python:3.12-slim は最小限のDebianベースで、pipやPython標準ライブラリは揃っている一方、gccなどは入っていません。ほとんどのWebアプリでは slim で十分です(3-6で詳説)。

4. ステップ3:マルチステージ化

今回の題材では依存関係に gcc を要するパッケージがないため、マルチステージの恩恵は小さめです。しかし、numpy や psycopg2 など、コンパイルを要するライブラリを追加した時に真価を発揮します。ここでは、現実的なケースを想定して psycopg2(PostgreSQLドライバ)を追加した前提で書きます。

requirements.txt(拡張版)

flask==3.0.0
 gunicorn==21.2.0
 psycopg2==2.9.9 # ← コンパイルが必要なパッケージ

Dockerfile v3

# Dockerfile (v3: マルチステージ化)
 
 # ===== ビルドステージ =====
 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 .
 # wheel を作成して /wheels に保存
 RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
 
 # ===== 実行ステージ =====
 FROM python:3.12-slim
 
 WORKDIR /app
 
 # 実行に必要な最小ライブラリのみ(libpq-dev ではなく libpq5)
 RUN apt-get update && apt-get install -y --no-install-recommends \
  libpq5 \
  && rm -rf /var/lib/apt/lists/*
 
 # wheel からインストール(gcc 不要)
 COPY --from=builder /wheels /wheels
 RUN pip install --no-cache-dir /wheels/*
 
 COPY . .
 
 EXPOSE 8000
 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

改善点

  • ビルド時のみ gcc/libpq-dev を使用(本番イメージには含まれない)
  • 実行ステージは libpq5(ランタイムライブラリ)だけで十分
  • 最終イメージサイズが 150MB → 130MB
  • セキュリティ向上:gcc が残らない

5. ステップ4:セキュリティ強化(非rootユーザー)

デフォルトのDockerコンテナは root ユーザーで動きます。脆弱性を突かれた際の被害を最小化するため、専用の非rootユーザーで実行するのが本番の鉄則です。

Dockerfile v4(完成形)

# Dockerfile (v4: 完成形)
 
 # ===== ビルドステージ =====
 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 wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
 
 # ===== 実行ステージ =====
 FROM python:3.12-slim
 
 # 非rootユーザーの作成
 RUN groupadd --system --gid 1001 app && \
  useradd --system --uid 1001 --gid app --no-create-home app
 
 WORKDIR /app
 
 RUN apt-get update && apt-get install -y --no-install-recommends \
  libpq5 \
  && rm -rf /var/lib/apt/lists/*
 
 COPY --from=builder /wheels /wheels
 RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
 
 # アプリファイルのコピー(所有者を app に)
 COPY --chown=app:app . .
 
 # 非rootユーザーに切替
 USER app
 
 EXPOSE 8000
 
 # ヘルスチェック
 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
 
 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "app:app"]

強化ポイント

強化項目 目的
非root ユーザー(USER app コンテナ侵害時の影響範囲を最小化
COPY --chown ファイル所有者を非rootに統一
HEALTHCHECK Docker・Kubernetesが異常を検知できる
--workers 2 gunicornのプロセス数を明示
rm -rf /wheels インストール後は wheel を削除
⚠️ 1024未満のポートをListenする場合
80 / 443 などの特権ポートをListenするには root権限が必要です。非rootユーザーで動かすなら 8000 / 8080 / 3000 などの非特権ポートを使い、外側でロードバランサー・リバースプロキシ(Nginx 等)が 80/443 を受け持つ構成にしましょう。

6. 4段階の比較:サイズ・ビルド時間・安全性

指標 v1 最小 v2 キャッシュ v3 マルチ v4 完成形
イメージサイズ 約 1GB 約 150MB 約 130MB 約 130MB
初回ビルド 60秒 40秒 45秒 45秒
コード1行変更後のビルド 40秒 3秒 3秒 3秒
.env 流出リスク ❌ あり ✅ なし ✅ なし ✅ なし
gcc 本番残存 ❌ あり ✅ なし ✅ なし
root で動作 ❌ yes ❌ yes ❌ yes ✅ no
ヘルスチェック ❌ なし ❌ なし ❌ なし ✅ あり
本番使用可否

同じアプリでも、Dockerfileの書き方次第でイメージサイズは 約 1/8、差分ビルドは 約 1/13 に。本番運用の観点では、v4 が最低ラインです。


7. docker build 実用コマンド集

コマンド 用途
docker build -t myapp:v1 . 基本ビルド(tagを付ける)
docker build -t myapp:v1 -t myapp:latest . 複数タグを同時に付与
docker build --no-cache -t myapp . キャッシュを使わずフルビルド
docker build --pull -t myapp . ベースイメージを最新化してビルド
docker build --target builder -t myapp:builder . 特定ステージまでビルド
docker build --build-arg KEY=value -t myapp . ARG に値を渡す
docker build -f Dockerfile.prod -t myapp . 別名Dockerfileを指定
docker build --progress=plain -t myapp . 詳細ログを表示
docker build --platform linux/amd64 -t myapp . 特定アーキテクチャ向けビルド
docker buildx build --platform linux/amd64,linux/arm64 -t myapp --push . マルチアーキビルド&push

ビルドコンテキストをカレント以外にする

# 親ディレクトリをコンテキスト、Dockerfileは docker/ サブディレクトリ
 docker build -f docker/Dockerfile -t myapp ..
 
 # Gitリポジトリから直接ビルド
 docker build -t myapp https://github.com/user/repo.git
 
 # 標準入力から Dockerfile を読み込む(コンテキストなし)
 echo "FROM alpine\nCMD echo hi" | docker build -

8. CI/CDへの繋ぎ方

作った Dockerfile を、GitHub Actions で自動ビルド・プッシュする最小構成例:

# .github/workflows/docker.yml
 name: Docker Build & Push
 
 on:
  push:
  branches: [main]
 
 jobs:
  build:
  runs-on: ubuntu-latest
  steps:
  - uses: actions/checkout@v4
 
  - uses: docker/setup-buildx-action@v3
 
  - uses: docker/login-action@v3
  with:
  registry: ghcr.io
  username: ${{ github.actor }}
  password: ${{ secrets.GITHUB_TOKEN }}
 
  - uses: docker/build-push-action@v5
  with:
  context: .
  push: true
  tags: ghcr.io/${{ github.repository }}:latest
  cache-from: type=gha
  cache-to: type=gha,mode=max

cache-from: type=gha でGitHub Actions のキャッシュ機構を使うと、ビルド時間がさらに短縮できます。詳しくは第12章「CI/CDパイプラインへの組み込み」で解説します。


9. まとめ

実プロジェクトのDockerfileは「正解が1つある」ものではなく、段階的に育てていくのが現実的です。このサイクルを身につければ、どんなアプリケーションでも本番品質のコンテナ化ができます。

段階 適用する知識 効果
v1 最小 3-1(基本構文) とにかく動かす
v2 キャッシュ 3-2(レイヤーキャッシュ)+3-3(.dockerignore) ビルド時間1/13
v3 マルチステージ 3-3(マルチステージビルド) ビルドツール除外
v4 セキュリティ 第8章(セキュリティ)の先取り 本番運用OK
✅ 次のステップ
3-6「イメージの軽量化戦略」では、ベースイメージ選び(full / slim / alpine / distroless / scratch)の判断基準と、さらなる軽量化テクニックを解説します。v4 の 130MB を 50MB 以下まで削る、最後の仕上げです。

参考リンク


Dockerの基礎を動画で体系的に学びませんか?

実務で使う基礎だけを3時間に凝縮。環境構築から丁寧に解説しています。

Udemy Docker入門講座 クーポン割引で講座を見る →

コメント

タイトルとURLをコピーしました