docker build 完全実践|Webアプリを一からDockerize するステップバイステップ
3-1〜3-4 でDockerfileの文法・レイヤーキャッシュ・マルチステージ・.dockerignore を学んできました。この記事ではそれら全てを 1つの実プロジェクトに適用し、本番で使える Dockerfile を段階的に育てていきます。
題材は Python Flask の小さなWeb API。最小構成から始めて、最適化・マルチステージ化・セキュリティ強化へと4段階で改善し、最終的にビルド時間と本番イメージサイズが両立された “プロ仕様” のDockerfileを完成させます。
目次
- 題材プロジェクトの構成
- ステップ1:最小構成(まず動かす)
- ステップ2:レイヤーキャッシュ最適化
- ステップ3:マルチステージ化
- ステップ4:セキュリティ強化(非rootユーザー)
- 4段階の比較:サイズ・ビルド時間・安全性
- docker build 実用コマンド集
- CI/CDへの繋ぎ方
- まとめ
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: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 を削除 |
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 build CLIリファレンス — 全オプションと使用例。
- GitHub Actions での Docker ビルド — 公式の CI/CD 連携ガイド。
- Flask 公式ドキュメント — 本記事の題材Flaskアプリの詳細。
- Gunicorn 公式 — PythonのWSGI HTTPサーバー(本番向け)。



コメント