Day 9: 実践的なDockerfile
今日学ぶこと
- マルチステージビルド
- イメージサイズの最適化
- セキュリティのベストプラクティス
- キャッシュの効率的な活用
マルチステージビルド
マルチステージビルドは、ビルド環境と実行環境を分離する技術です。これにより、最終イメージのサイズを大幅に削減できます。
問題:ビルドツールが残る
通常のDockerfileでは、ビルドに必要なツールがイメージに残ってしまいます。
# 従来の方法(問題あり)
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build # TypeScriptをコンパイル
CMD ["node", "dist/index.js"]
# 問題: node_modules(開発用含む)、TypeScriptコンパイラなどが残る
# イメージサイズ: 約1GB以上
解決:マルチステージビルド
# ステージ1: ビルド環境
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# ステージ2: 実行環境
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production
COPY /app/dist ./dist
CMD ["node", "dist/index.js"]
# 結果: ビルドツール不要、本番依存のみ
# イメージサイズ: 約200MB
flowchart LR
subgraph Stage1["ステージ 1: builder"]
S1["ソースコード\n+ 開発依存"]
B1["ビルド実行"]
O1["成果物\n(dist/)"]
end
subgraph Stage2["ステージ 2: 最終イメージ"]
P1["本番依存のみ"]
O2["成果物コピー"]
Final["最小限の\n実行環境"]
end
S1 --> B1 --> O1
O1 --> |"COPY --from=builder"| O2
P1 --> Final
O2 --> Final
style Stage1 fill:#f59e0b,color:#fff
style Stage2 fill:#22c55e,color:#fff
Goアプリケーションの例
Goは特にマルチステージビルドの効果が大きいです。
# ステージ1: ビルド
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# ステージ2: 実行(scratch = 空のイメージ)
FROM scratch
COPY /app/main /main
ENTRYPOINT ["/main"]
# イメージサイズ: 約10MB(Goランタイム不要)
| ステージ | サイズ |
|---|---|
| golang:1.22 | 約800MB |
| 最終イメージ(scratch) | 約10MB |
イメージサイズの最適化
1. 軽量ベースイメージを選ぶ
# ❌ フルイメージ(約1GB)
FROM python:3.11
# ✅ slimイメージ(約150MB)
FROM python:3.11-slim
# ✅ Alpineイメージ(約50MB)
FROM python:3.11-alpine
2. レイヤーを最小限に
各 RUN 命令はレイヤーを作成します。可能な限りまとめましょう。
# ❌ 悪い例:レイヤーが多い
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*
# ✅ 良い例:1つのRUNにまとめる
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git && \
rm -rf /var/lib/apt/lists/*
3. 不要なファイルを含めない
.dockerignore
.git
node_modules
*.log
.env
.DS_Store
__pycache__
*.pyc
.pytest_cache
coverage/
dist/
build/
4. キャッシュを削除
# Python
RUN pip install --no-cache-dir -r requirements.txt
# Node.js
RUN npm ci --only=production && npm cache clean --force
# apt
RUN apt-get update && \
apt-get install -y package && \
rm -rf /var/lib/apt/lists/*
# apk (Alpine)
RUN apk add --no-cache package
キャッシュの効率的な活用
依存ファイルを先にコピー
変更頻度の低いファイルを先にコピーすることで、キャッシュを最大限活用できます。
# ✅ 良い例
WORKDIR /app
# 1. 依存定義ファイルをコピー(変更頻度:低)
COPY package.json package-lock.json ./
# 2. 依存をインストール(キャッシュされる)
RUN npm ci
# 3. ソースコードをコピー(変更頻度:高)
COPY . .
# ソースコードだけ変更した場合、ステップ1-2はキャッシュが使われる
flowchart TB
subgraph Build1["初回ビルド"]
B1A["COPY package.json"] --> B1B["RUN npm ci"]
B1B --> B1C["COPY . ."]
end
subgraph Build2["ソース変更後の2回目"]
B2A["COPY package.json\n(キャッシュ)"] --> B2B["RUN npm ci\n(キャッシュ)"]
B2B --> B2C["COPY . .\n(再実行)"]
end
style B2A fill:#22c55e,color:#fff
style B2B fill:#22c55e,color:#fff
style B2C fill:#f59e0b,color:#fff
BuildKitのキャッシュマウント
# BuildKitの機能を使ったキャッシュ
RUN \
pip install -r requirements.txt
RUN \
npm ci
セキュリティのベストプラクティス
1. 非rootユーザーで実行
FROM node:20-slim
# ユーザーを作成
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /app
COPY . .
# ユーザーを切り替え
USER appuser
CMD ["node", "index.js"]
2. 最小限の権限
# ファイルの権限を制限
COPY script.sh /app/
3. シークレットを含めない
# ❌ 絶対にやってはいけない
COPY .env /app/
ENV API_KEY=secret123
# ✅ 実行時に環境変数として渡す
# docker run -e API_KEY=xxx myimage
4. 特定バージョンを固定
# ❌ 悪い例
FROM python:latest
RUN pip install flask
# ✅ 良い例
FROM python:3.11.7-slim-bookworm
RUN pip install flask==3.0.0
5. 脆弱性スキャン
# Docker Scoutで脆弱性スキャン
docker scout cves myimage:latest
# Trivyを使用
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myimage:latest
実践的なDockerfileパターン
Pythonアプリケーション
# マルチステージビルド
FROM python:3.11-slim AS builder
WORKDIR /app
# 仮想環境を作成
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 最終イメージ
FROM python:3.11-slim
# 非rootユーザー
RUN useradd --create-home appuser
USER appuser
WORKDIR /app
# 仮想環境をコピー
COPY /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
Node.jsアプリケーション
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
# 最終イメージ
FROM node:20-slim
RUN useradd --create-home appuser
USER appuser
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
Reactフロントエンド + Nginx
# ビルドステージ
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Nginxで配信
FROM nginx:alpine
COPY /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
HEALTHCHECK命令
コンテナの健全性を監視します。
FROM nginx:alpine
HEALTHCHECK \
CMD curl -f http://localhost/ || exit 1
| オプション | 説明 |
|---|---|
--interval |
チェック間隔 |
--timeout |
タイムアウト時間 |
--start-period |
起動待機時間 |
--retries |
失敗許容回数 |
LABEL命令でメタデータを追加
FROM python:3.11-slim
LABEL maintainer="your@email.com"
LABEL version="1.0.0"
LABEL description="My awesome app"
LABEL org.opencontainers.image.source="https://github.com/user/repo"
イメージサイズの確認
# イメージサイズを確認
docker images myapp
# 詳細な分析
docker history myapp:latest
# diveツールでレイヤー分析
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive myapp:latest
まとめ
| テクニック | 効果 |
|---|---|
| マルチステージビルド | ビルドツールを除外、サイズ削減 |
| 軽量ベースイメージ | 基本サイズを削減 |
| レイヤーの統合 | レイヤー数とサイズを削減 |
| .dockerignore | 不要ファイルを除外 |
| キャッシュ削除 | 一時ファイルを除外 |
| 依存を先にコピー | ビルドキャッシュを活用 |
| 非rootユーザー | セキュリティ向上 |
| バージョン固定 | 再現性とセキュリティ |
重要ポイント
- マルチステージビルドで最終イメージを最小化
- 変更頻度の低いものを先にコピーしてキャッシュ活用
- 必ず非rootユーザーで実行
- シークレットはイメージに含めない
- 本番環境では具体的なバージョンを固定
練習問題
問題1: マルチステージビルド
TypeScriptのNode.jsアプリ用のマルチステージDockerfileを作成してください:
- ステージ1: ビルド(npm run build)
- ステージ2: 本番依存のみで実行
問題2: 最適化
以下のDockerfileの問題点を指摘し、改善してください:
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
RUN apt-get update
RUN apt-get install -y curl
CMD ["python", "app.py"]
チャレンジ問題
Goアプリケーション用のマルチステージDockerfileを作成し、最終イメージを10MB以下にしてください。ヒント:scratch イメージを使用します。
参考リンク
次回予告: Day 10では「本番環境への準備とベストプラクティス」について学びます。セキュリティ、ログ、モニタリング、そして次のステップとしてKubernetesへの橋渡しを行います。