Dockerfile最適化とは

Dockerfile最適化は、イメージサイズの削減とビルド時間の短縮を目指します。

最適化のメリット

  • 📦 デプロイ時間の短縮
  • 💰 ストレージコスト削減
  • 🚀 コンテナ起動時間の短縮
  • 🔒 攻撃面の縮小(セキュリティ向上)
  • 🌐 ネットワーク転送量の削減

ベースイメージの選択

Alpine Linuxの使用

# ❌ 大きいイメージ(~900MB)
FROM node:18

# ✅ Alpine版(~170MB)
FROM node:18-alpine

# ✅ さらに小さい slim版(~250MB)
FROM node:18-slim

サイズ比較

  • node:18: ~900MB
  • node:18-slim: ~250MB
  • node:18-alpine: ~170MB

distrolessイメージ

# Google製の最小限イメージ
FROM gcr.io/distroless/nodejs:18

# シェルすら含まない(セキュリティ向上)
# デバッグは難しくなる

マルチステージビルド

基本的なマルチステージ

# ビルドステージ
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 本番ステージ
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

効果

Before: 1.2GB(ビルドツール含む)
After:  180MB(実行に必要なもののみ)

Python マルチステージ例

# ビルドステージ
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 本番ステージ
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

レイヤーキャッシュの活用

変更頻度の低い順に配置

# ❌ 悪い例(毎回フルビルド)
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

# ✅ 良い例(依存関係をキャッシュ)
FROM node:18-alpine
WORKDIR /app

# 依存関係ファイルのみコピー(変更少ない)
COPY package*.json ./
RUN npm ci

# ソースコードをコピー(変更多い)
COPY . .
RUN npm run build

キャッシュが効く理由

1回目のビルド:
1. COPY package*.json  → 実行
2. RUN npm ci          → 実行
3. COPY .              → 実行
4. RUN npm run build   → 実行

2回目(ソースのみ変更):
1. COPY package*.json  → キャッシュ使用
2. RUN npm ci          → キャッシュ使用
3. COPY .              → 実行
4. RUN npm run build   → 実行

不要なファイルの除外

.dockerignoreの活用

# .dockerignore

node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
README.md
.vscode
.idea
__pycache__
*.pyc
*.pyo
*.log
.DS_Store
dist
build
.cache
coverage
.pytest_cache

# ドキュメント
*.md
!README.md

# テストファイル
test/
tests/
__tests__/
*.test.js
*.spec.js

効果

  • COPY時の転送データ削減
  • イメージサイズ削減
  • ビルド時間短縮

RUNコマンドの最適化

複数コマンドの結合

# ❌ 悪い例(レイヤーが増える)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean

# ✅ 良い例(1レイヤー)
RUN apt-get update && \
    apt-get install -y \
      curl \
      vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

キャッシュの削除

# Alpine Linux
RUN apk add --no-cache python3 py3-pip

# Debian/Ubuntu
RUN apt-get update && \
    apt-get install -y python3 && \
    rm -rf /var/lib/apt/lists/*

# Python pip
RUN pip install --no-cache-dir -r requirements.txt

# npm
RUN npm ci --only=production && \
    npm cache clean --force

セキュリティベストプラクティス

非rootユーザーで実行

FROM node:18-alpine

WORKDIR /app

# ユーザー作成
RUN addgroup -g 1001 appgroup && \
    adduser -D -u 1001 -G appgroup appuser

# ファイルコピー
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci
COPY --chown=appuser:appgroup . .

# ユーザー切り替え
USER appuser

EXPOSE 3000
CMD ["node", "index.js"]

秘密情報の扱い

# ❌ 悪い例(イメージに残る)
RUN echo "API_KEY=secret123" > .env

# ✅ 良い例(ビルド引数)
ARG API_KEY
RUN configure --api-key=$API_KEY

# ✅ または実行時に環境変数で渡す
ENV API_KEY=${API_KEY}

# Docker Secretsを使う(Swarm/Compose)
# docker run -e API_KEY="$API_KEY" myapp

ビルド引数と環境変数

ARG vs ENV

# ARG: ビルド時のみ有効
ARG NODE_ENV=production
RUN if [ "$NODE_ENV" = "production" ]; then \
      npm ci --only=production; \
    else \
      npm ci; \
    fi

# ENV: 実行時も有効
ENV NODE_ENV=production
ENV PORT=3000

# 両方使う
ARG VERSION=1.0.0
ENV APP_VERSION=$VERSION

条件付きビルド

ARG BUILD_ENV=production

FROM node:18-alpine AS base
WORKDIR /app

# 開発用ステージ
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

# 本番用ステージ
FROM base AS production
RUN npm ci --only=production
COPY . .
RUN npm run build
CMD ["npm", "start"]

# 選択
FROM ${BUILD_ENV} AS final

ヘルスチェック

HEALTHCHECKの設定

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js || exit 1

EXPOSE 3000
CMD ["node", "index.js"]

healthcheck.jsの例

// healthcheck.js
const http = require('http');

const options = {
  host: 'localhost',
  port: 3000,
  path: '/health',
  timeout: 2000
};

const request = http.request(options, (res) => {
  if (res.statusCode === 200) {
    process.exit(0);
  } else {
    process.exit(1);
  }
});

request.on('error', () => process.exit(1));
request.end();

実践的なDockerfile例

Node.js アプリケーション

FROM node:18-alpine AS builder

WORKDIR /app

# 依存関係インストール
COPY package*.json ./
RUN npm ci

# ビルド
COPY . .
RUN npm run build && \
    npm prune --production

# 本番イメージ
FROM node:18-alpine

WORKDIR /app

# 非rootユーザー
RUN addgroup -g 1001 nodejs && \
    adduser -D -u 1001 -G nodejs nodejs

# ビルド成果物コピー
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"

CMD ["node", "dist/index.js"]

Python Flask アプリケーション

FROM python:3.11-slim AS builder

WORKDIR /app

# 依存関係インストール
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 本番イメージ
FROM python:3.11-slim

WORKDIR /app

# 非rootユーザー
RUN useradd -m -u 1001 appuser

# Pythonパッケージコピー
COPY --from=builder --chown=appuser:appuser /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .

USER appuser

ENV PATH=/home/appuser/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"

CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]

ビルド時間の測定

ビルド時間の確認

# 通常ビルド
time docker build -t myapp .

# キャッシュなしビルド
time docker build --no-cache -t myapp .

# BuildKitで詳細表示
DOCKER_BUILDKIT=1 docker build -t myapp .

イメージサイズの確認

# イメージ一覧
docker images

# 詳細表示
docker history myapp:latest

# レイヤーごとのサイズ
docker history --no-trunc myapp:latest

BuildKitの活用

BuildKit有効化

# 環境変数で有効化
export DOCKER_BUILDKIT=1

# または docker-compose.yml で
version: '3.8'
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - DOCKER_BUILDKIT=1

BuildKitの機能

# 並列ビルド
DOCKER_BUILDKIT=1 docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t myapp .

# Secretsマウント
# syntax=docker/dockerfile:1
FROM alpine
RUN --mount=type=secret,id=mysecret \
    cat /run/secrets/mysecret

# docker build --secret id=mysecret,src=secret.txt .

# キャッシュマウント
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

まとめ

Dockerfile最適化でイメージサイズを削減し、ビルドを高速化できます。

重要ポイント

  • Alpine/Slimイメージを使用
  • マルチステージビルドで分離
  • レイヤーキャッシュを活用
  • .dockerignoreで不要ファイル除外
  • 非rootユーザーで実行

チェックリスト

  • [ ] ベースイメージは最小限か
  • [ ] マルチステージビルドを使っているか
  • [ ] .dockerignoreを設定したか
  • [ ] RUNコマンドを結合したか
  • [ ] キャッシュを削除したか
  • [ ] 非rootユーザーで実行するか
  • [ ] ヘルスチェックを設定したか