3.6 Dockerイメージ軽量化の完全ガイド|full・slim・alpine・distroless・scratch の使い分け

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

Dockerイメージ軽量化の完全ガイド|full・slim・alpine・distroless・scratch の使い分け

「Docker イメージを 小さくしたい」——これは単にディスク容量の話ではありません。軽量化は 起動速度・レジストリ転送コスト・攻撃対象の最小化・CI効率 すべてを同時に改善する、Docker運用の最終仕上げです。

この記事では、ベースイメージ選択の基準(full / slim / alpine / distroless / scratch)と、実際にサイズを削るテクニックを、落とし穴も含めて総合的に解説します。3-4で作った Flask アプリを 130MB → 50MB以下まで削る実例も示します。


目次

  1. なぜ軽量化するのか(5つの理由)
  2. ベースイメージの選択肢マップ
  3. alpine の仕組みと注意点
  4. distroless(Googleの「アプリだけ」イメージ)
  5. scratch(究極のゼロベース)
  6. 軽量化テクニック一覧
  7. 実例:Flaskアプリを 130MB → 50MB に
  8. サイズの測り方・分析ツール
  9. 軽量化の落とし穴
  10. まとめ

1. なぜ軽量化するのか(5つの理由)

理由 具体的な影響
① 起動速度 レジストリからのpullが速い → オートスケール時の応答性向上
② 転送コスト CI/CD、デプロイ、エッジ配布の帯域とストレージを削減
③ 攻撃対象の最小化 不要なバイナリ・ライブラリが少ない=脆弱性の露出が減る
④ イメージスキャン速度 Trivy 等のCVEスキャンが高速に完了
⑤ ストレージコスト レジストリ・ノードのディスク使用量削減
💡 最大の狙いは「攻撃対象の最小化」
サイズ削減はコスト削減として語られがちですが、本質的な価値は セキュリティです。bashcurlapt などが入っていないイメージは、侵害されても攻撃者ができることが極めて限られます(第8章 セキュリティで詳説)。

2. ベースイメージの選択肢マップ

代表的なベースイメージの特性を整理します。Python を例にした比較です。

種別 代表タグ サイズ 同梱 向き
full python:3.12 約 1GB Debian full、開発ツール多数 開発時・デバッグ用
slim python:3.12-slim 約 130MB Debian最小、python+標準lib 本番のデフォルト候補
alpine python:3.12-alpine 約 55MB Alpine Linux (musl libc) 軽量最優先・要注意
distroless gcr.io/distroless/python3-debian12 約 50MB Pythonランタイムのみ(shellなし) 本番・セキュリティ重視
scratch scratch 0 バイト 何もない 静的バイナリ専用(Go等)

選択フローチャート

┌─────────────────────────────┐
│ 完全な静的バイナリを作れる? │
│ (Go, Rust 等) │
└────┬────────────────┬───────┘
│ YES │ NO
▼ ▼
┌──────┐ ┌─────────────────────────┐
│scratch│ │ Pythonのような動的ランタイム │
└──────┘ └─────┬───────────┬───────┘
│ │
セキュリティ最優先? 軽量最優先?
│ │
▼ ▼
┌──────────┐ ┌──────┐
│distroless│ │alpine│
└──────────┘ └──────┘
↑ │
│ │
│ musl互換性に問題?
└──── YES───┘
│ NO

┌──────────┐
│ slim │
│(無難な中間)│
└──────────┘

3. alpine の仕組みと注意点

Alpine Linux は musl libcBusyBox を使った超軽量ディストリビューション(約 5MB)です。Dockerイメージのベースとして広く使われていますが、従来のLinuxと互換性が完全ではない点に注意が必要です。

alpine のメリット

  • 極めて小さい(ベース約 7MB、python:alpine でも 55MB)
  • 攻撃対象面積が狭い
  • セキュリティアップデートが頻繁

alpine のデメリット・注意点

注意点 詳細
musl libc と glibc の非互換 glibc 前提のバイナリ・ライブラリが動かないことがある
pip のビルド時間が長い 多くのPythonホイールがmanylinux(glibc)向けで、alpine ではソースからビルドしなおし
DNSまわりの挙動差 musl の getaddrinfo は /etc/resolv.conf の扱いが glibc と異なる
時刻・ロケール tzdata・locale が別途インストールが必要な場合がある
パッケージマネージャが apk apt-get とは記法が異なる
⚠️ Pythonで alpine を使うときの落とし穴
Python の pip で numpy・pandas・Pillow などを alpine にインストールすると、ホイールが存在せずソースビルドになるため、ビルド時間が10倍以上かかることがあります。Pythonなら slim の方が結果的に速くて小さいケースもあります。思い込みで alpine を選ばず、実測で比較しましょう。

apk(Alpine のパッケージマネージャ)の使い方

# apt-get update && apt-get install -y foo の代わりに:
 RUN apk add --no-cache foo
 
 # ビルド専用パッケージの一時導入(alpine特有の便利機能)
 RUN apk add --no-cache --virtual .build-deps gcc musl-dev \
  && pip install psycopg2 \
  && apk del .build-deps
 # ^ ビルド後に .build-deps をまるごと削除できる

4. distroless(Googleの「アプリだけ」イメージ)

distroless は Google が提供するベースイメージで、アプリとそのランタイムライブラリのみを含みます。shell(bash/sh)・curlls すらありません。

distroless の特徴

  • サイズが小さい(Pythonで 約 50MB)
  • shellがないのでexec攻撃がほぼ不可能
  • パッケージマネージャもないので勝手にインストールされない
  • root ではなく nonroot ユーザーがデフォルト

使用例(マルチステージとの組み合わせ)

# ===== ビルド =====
 FROM python:3.12-slim AS builder
 WORKDIR /app
 COPY requirements.txt .
 RUN pip install --user --no-cache-dir -r requirements.txt
 COPY . .
 
 # ===== 実行(distroless)=====
 FROM gcr.io/distroless/python3-debian12:nonroot
 WORKDIR /app
 # pip の --user でインストールしたパッケージをコピー
 COPY --from=builder /root/.local /home/nonroot/.local
 COPY --from=builder /app /app
 ENV PATH=/home/nonroot/.local/bin:$PATH
 CMD ["app.py"]
💡 debug タグでシェルが使える
distroless は :debug タグにすると BusyBox シェルが入るため、トラブルシューティング時だけ debug に切り替えるという運用ができます(例:gcr.io/distroless/python3-debian12:debug)。本番は shell なし、調査時だけ debug、が鉄則。

5. scratch(究極のゼロベース)

scratch は「何もない」特別なイメージで、サイズは 0バイトです。静的バイナリにコンパイルできる言語(Go・Rust)でのみ実用的です。

# Go アプリを scratch で動かす最小構成
 FROM golang:1.22-alpine AS builder
 WORKDIR /src
 COPY . .
 RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/app
 
 FROM scratch
 # TLSを使うなら ca-certificates を明示的にコピー
 COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
 COPY --from=builder /bin/app /app
 ENTRYPOINT ["/app"]
 
 # 最終イメージ: Goバイナリ(約5MB)+ CA証明書(約200KB)= 約5MB

scratch の注意点

  • shell、libc、何もないので docker exec でコマンドを打つことすらできない
  • 動的リンクのバイナリは絶対に動かない(静的のみ)
  • HTTPS通信するなら CA証明書を自分でコピーする必要がある
  • タイムゾーン情報 (/usr/share/zoneinfo) も必要なら明示的に

6. 軽量化テクニック一覧

テクニック 効果 適用章との関連
slim / alpine / distroless ベースに変更 数百MB単位の削減 本章
マルチステージビルド ビルドツールを除外 3-5
RUN の && 連結+キャッシュ削除 中間ファイル削減 3-2
--no-install-recommends(apt) 推奨パッケージを入れない 本章
rm -rf /var/lib/apt/lists/* aptキャッシュ削除 本章
pip install --no-cache-dir pip キャッシュを残さない 本章
Go: -ldflags="-s -w" シンボル情報を削除(バイナリ縮小) 本章
Go: UPX 圧縮 バイナリをさらに圧縮(起動時に展開) 本章
.dockerignore 不要ファイルを含めない 3-3
イメージから不要ツール削除 gcc, make, dev-libs 等を build-stage のみに 3-3

apt のキャッシュ削除パターン

RUN apt-get update && \
  apt-get install -y --no-install-recommends \
  curl \
  ca-certificates && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/*

7. 実例:Flaskアプリを 130MB → 50MB に

3-4 で作った Flask アプリ(slim ベース、130MB)を distroless で書き直してみます。

# ===== ビルドステージ =====
 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 .
 # --user で /root/.local 以下に集約(後で distroless にコピーしやすい)
 RUN pip install --user --no-cache-dir -r requirements.txt
 
 COPY . .
 
 # ===== 実行ステージ(distroless)=====
 FROM gcr.io/distroless/python3-debian12:nonroot
 
 WORKDIR /app
 
 # 依存ライブラリ(/root/.local → /home/nonroot/.local)
 COPY --from=builder /root/.local /home/nonroot/.local
 # アプリコード
 COPY --from=builder /app /app
 
 ENV PATH=/home/nonroot/.local/bin:$PATH
 ENV PYTHONPATH=/home/nonroot/.local/lib/python3.12/site-packages
 
 EXPOSE 8000
 
 CMD ["-m", "gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
 
 # 最終サイズ: 約 50MB
バージョン ベース サイズ shell セキュリティ
v4(3-5完成形) python:slim 約 130MB あり
v5(本記事) distroless 約 50MB なし
⚠️ distroless に移行する前に確認すべきこと
distroless は shell がないため、docker exec -it myapp bash のようなデバッグができません。トラブルシューティングはログ・ヘルスチェック・メトリクスに頼る必要があります。ログが整備されていないチームは、まず slim で本番運用して知見を貯めてから distroless に移行するのが安全です。

8. サイズの測り方・分析ツール

基本:docker images

$ docker images myapp
 REPOSITORY TAG IMAGE ID CREATED SIZE
 myapp v1 abc123 2 minutes ago 1.02GB
 myapp v5 def456 1 minute ago 52MB

レイヤー単位の分析:docker history

$ docker history myapp:v5 --human --format "table {{.CreatedBy}}\t{{.Size}}"
 CREATED BY SIZE
 CMD ["-m" "gunicorn" ...] 0B
 ENV PYTHONPATH=/home/nonroot/.local/lib/... 0B
 COPY /app /app 3.2kB
 COPY /root/.local /home/nonroot/.local 38MB ← ここが重い
 ...

詳細分析:dive(推奨)

dive は各レイヤーの中身を視覚的に調べられるツールです。

# インストール(Linux/macOS)
 brew install dive
 
 # 使い方
 dive myapp:v5
 # → レイヤーごとに何が追加・変更・削除されたか可視化される

9. 軽量化の落とし穴

落とし穴 症状 対策
alpine 盲信でビルド時間増大 pip でnumpyをソースビルド → CIが20分に slim と実測で比較、alpineが常に正解ではない
scratch で HTTPS が動かない “x509: certificate signed by unknown authority” CA証明書をbuildステージから COPY
distroless でデバッグができない コンテナ内でコマンドを打てない :debug タグに切替 or ログ整備
apt-get install の後でクリーンしていない イメージが数百MB肥大化 rm -rf /var/lib/apt/lists/* 必須
タイムゾーンが UTC のまま ログのタイムスタンプがずれる tzdata を入れて TZ 環境変数設定
latest タグで alpine が更新された結果、互換性問題 ある日突然ビルドが壊れる タグをバージョン固定(alpine:3.19
イメージは小さいがメモリ使用量は減らない 「軽い=省メモリ」ではない メモリ削減は第9章 リソース制限で別途対応

10. まとめ

軽量化はサイズ数字を小さくすること自体が目的ではなく、起動速度・セキュリティ・運用コストを改善する手段です。適切なベースイメージ選びと、マルチステージ・レイヤーキャッシュ・.dockerignore の3つを組み合わせると、本番品質の最小イメージが作れます。

シナリオ 推奨ベース 理由
初めてのDockerfile・開発環境 full デバッグツールが揃っている
一般的なWebアプリ(本番) slim 互換性◎・サイズ◎のバランス
サイズ最優先・glibc依存なし alpine 最小。ただしPythonは要注意
セキュリティ最優先(本番) distroless shell なし、攻撃面最小
Go/Rustの静的バイナリ scratch 究極の軽さ(5MB〜)
✅ 第3章のまとめと次章へ
これで第3章「Dockerfileによるイメージ構築」は完了です。3-1 で文法、3-2 でキャッシュ、3-3 でマルチステージ、3-3 で .dockerignore、3-5 で実践、3-6 で軽量化——本番運用可能なDockerfileを書く全要素が揃いました。

第4章「データの永続化」では、コンテナのエフェメラル性(消えてしまう性質)に向き合い、ボリューム・バインドマウント・tmpfs を使ってデータを守る運用技術を解説します。アプリと同じくらい重要なテーマです。

参考リンク


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

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

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

コメント

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