本番コンテナのセキュリティスキャンを初めて実行して、Critical CVEが数十件ヒットして青ざめた経験を持つエンジニアは少なくありません。その原因の多くが、Dockerfileのベースイメージ選択を深く考えていなかったことです。
この記事では、
FROM命令で選べる主要なベースイメージ──debian・debian:slim・ubuntu・alpine・distroless・scratch──を実測データで比較し、アプリ種別ごとの選定ルールを整理します。動作確認環境: Docker 27.3 / RHEL 9.4・Ubuntu 24.04 LTS
この記事のポイント
・FROM命令のベースイメージ選択でCVE数・サイズ・デバッグ性が決まる
・debian:slimはPythonアプリでの互換性とサイズのバランスが最良
・alpineはmusl libcでglibcバイナリが動かず選択前に要確認
・distrolessはCVEを最小化できるがシェルがなくデバッグ設計が必要
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
ベースイメージ選択がDockerfileの品質を左右する理由
DockerfileのFROM命令は、「このコンテナに最初から積み込むOSの部品セットを選ぶ」命令です。たとえば
FROM debian:12と書くと、Debian 12(Bookworm)の最小インストール環境が丸ごと取り込まれます。その中にはglibcやbash、aptといった基本コンポーネントが含まれますが、同時にそれらのパッケージに含まれる既知の脆弱性(CVE)も一緒に持ち込まれます。逆に
FROM scratchと書けば、何も入っていない空のイメージから始まるため、CVEはゼロですがシェルもライブラリも存在しません。ベースイメージを選ぶ基準は主に3つです。
・イメージサイズ: 展開後のサイズが大きいほど、レジストリへのpush/pull・コンテナ起動に時間がかかります。CI/CDパイプラインでは積み重なって大きなコストになります
・CVE数: ベースに含まれるパッケージが多いほど、脆弱性スキャンの結果が悪化します。セキュリティポリシーが厳しい環境では選定理由を説明できなければなりません
・デバッグ容易性: シェルやコマンドがないイメージはCVEが少ない反面、コンテナ内に入って調査することができません。観測性(Observability)の設計が必要になります
この3つのバランスをどこに置くかで、最適なベースイメージが変わります。Dockerfileを書く前に「本番のセキュリティ要件」と「運用中のデバッグ手順」を先に決めておくと、選択が迷いなくなります。
主要ベースイメージのサイズとCVE数を比較する
以下はdocker pull後にdocker image inspectで取得した展開サイズと、trivy image --severity HIGH,CRITICALでスキャンしたCVE件数の実測値です(trivy v0.55.0)。スキャンするタイミングによってパッチ状況が変わるため、あくまで傾向値として参照してください。各イメージのベンダーがセキュリティパッチを適用するたびに数値は変動します。
| イメージ | 圧縮サイズ | 展開サイズ | HIGH+CRITICAL CVE(目安) |
|---|---|---|---|
| ubuntu:24.04 | 約29MB | 約78MB | 5~20件 |
| debian:12 | 約52MB | 約137MB | 10~30件 |
| debian:12-slim | 約27MB | 約74MB | 3~15件 |
| alpine:3.20 | 約3.7MB | 約7.4MB | 0~2件 |
| gcr.io/distroless/base-debian12 | 約20MB | 約49MB | 0~3件 |
| scratch | 0MB | 0MB | 0件 |
1. debian:12とdebian:12-slim
debian:12(Bookworm)は、多くのツールが最初から入っている「フルDebian」です。ドキュメント、マニュアルページ、各種ユーティリティが揃っているため、インタラクティブな作業環境としては使いやすい反面、サイズが大きくCVEも多めです。debian:12-slimは不要なファイルを除いた最小構成です。aptは使えるためパッケージの追加インストールは可能ですが、curlやwgetはデフォルトでは含まれません。フルDebianとの互換性を保ちつつサイズを半分近くに抑えられるため、PythonアプリやJavaアプリのデフォルト候補として広く使われています。# debian:12-slimベースのPythonアプリDockerfile例 FROM debian:12-slim RUN apt-get update && apt-get install -y --no-install-recommends \ python3 python3-pip \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip3 install --no-cache-dir -r requirements.txt COPY . . CMD ["python3", "app.py"]
apt-get installの後にrm -rf /var/lib/apt/lists/*を付けてキャッシュを削除するのがセットです。これを省略するとaptのキャッシュがレイヤーに残り、数十MBが無駄に膨らみます。2. ubuntu:24.04
ubuntu:24.04はDebianをベースにしたUbuntu公式イメージです。aptが使え、Debianとほぼ同じ操作感で扱えます。ただしDebianより多くのパッケージが含まれており、slimより大きめのサイズになります。Ubuntu独自のパッケージ(
software-properties-commonなど)が必要な場合や、チームがUbuntuに慣れている場合に選択します。DockerfileをRHEL/Rocky系の実機サーバーと共存させる場合は、パッケージ名の差異(python3-dev vs python3-devel)に注意してください。3. alpine:3.20
Alpineは圧倒的な小ささが特徴です。musl libcとbusyboxを組み合わせた独自の最小Linuxで、展開後も7MB台に収まります。セキュリティスキャンの結果もクリーンになりやすく、一見理想的に見えます。ただし、musl libcとglibc(GNU C Library)の非互換という落とし穴があります。次の「alpineを使う時に必ずハマるmusl libc問題」セクションを先に確認してから採用を検討してください。
4. gcr.io/distroless/base-debian12(Distroless)
Googleが提供するDistrolessイメージは、シェル・パッケージマネージャー・デバッグツールを一切含まない最小構成です。アプリの実行に必要なランタイムライブラリだけを含めるため、CVEが激減します。利用可能なイメージの種類は用途別に分かれています。
・
gcr.io/distroless/base-debian12: glibc・ssl・tzdataだけの最小ベース・
gcr.io/distroless/python3-debian12: Python 3インタープリタ付き・
gcr.io/distroless/java21-debian12: JRE 21付き・
gcr.io/distroless/static-debian12: 静的バイナリ向け(glibc不要)・
gcr.io/distroless/cc-debian12: C/C++の動的リンクバイナリ向けDistrolessはGitHubの
GoogleContainerTools/distrolessリポジトリで管理されており、イメージはGCR(Google Container Registry)から取得します。5. scratch
FROM scratchは何も含まない空のイメージです。Goなどで完全静的にコンパイルしたバイナリだけをCOPYすれば、シェルもライブラリも持たない超小型イメージが作れます。CVEはゼロになりますが、コンテナに入ってデバッグすることが完全にできなくなります。本番用の静的バイナリ配布には有効ですが、観測性(ログのstdout集約・メトリクスエンドポイント)を設計してから採用するのが現実的です。
alpineを使う時に必ずハマるmusl libc問題
AlpineはMicrosoft・Googleが提供する公式言語イメージ(python:3.12-alpine・node:20-alpine)でも採用されているため、「小さいイメージ = alpine使えばいい」と思われがちです。しかし現場ではmusl libc起因のトラブルが後を絶ちません。問題の核心: Alpineが使う
musl libcは、多くのLinuxバイナリが依存するglibcとABI互換がありません。コンパイル済みのglibcバイナリをAlpineに持ち込もうとすると、共有ライブラリが見つからずに起動に失敗します。# alpine上でglibcバイナリを実行しようとした時のエラー例 $ ./myapp sh: ./myapp: not found # ライブラリの依存を確認(ビルド元ホストでの確認例) $ ldd ./myapp linux-vdso.so.1 (0x...) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x...) # alpineには /lib/x86_64-linux-gnu/libc.so.6 が存在しない
.whl)ファイルがmanylinux(glibc依存)でビルドされているケースがよくあります。Alpineでは対応するWheelが見つからず、ソースからのビルドにフォールバックします。numpyやcryptographyなどで起こると、ビルドに数十分かかったり、コンパイラが足りずにエラーになったりします。# alpine上でnumpyをpip installしようとした時のよくあるエラー $ pip install numpy ... ERROR: Could not build wheels for numpy, which is required to install... error: command '/usr/bin/gcc' failed with exit code 1 # 対処: apkでビルドツールを追加する(イメージが大きくなる) RUN apk add --no-cache gcc musl-dev python3-dev
debian:12-slimに変更するほうが根本解決になります。alpineが向いているケース:
・シェルスクリプトとalpineネイティブパッケージだけで完結するツール
・glibcへの依存がない純粋なGoバイナリやRustバイナリ(静的ビルド)
・外部ネイティブ拡張に依存しないシンプルなNode.jsアプリ
alpineを避けるべきケース:
・Python(特にnumpy・pandas・scikit-learn等の科学系ライブラリ)
・Javaアプリ(JREはglibc前提)
・ネイティブ拡張を多用するRubyアプリ
distrolessイメージとシェルなし運用の設計指針
Distrolessイメージはセキュリティを重視する現場で採用が増えています。シェルがないためdocker exec -it コンテナ名 bashが使えず、デバッグ方法を事前に設計しておく必要があります。1. マルチステージビルドで開発用と本番用を分ける
Distrolessを本番ステージに使い、ビルドや開発ステージには通常のイメージを使います。マルチステージビルドとの組み合わせが前提になります。# ビルドステージ: debian:12-slimでビルド・依存インストール FROM debian:12-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends \ python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN python3 -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" RUN pip install --no-cache-dir -r requirements.txt COPY . . # 本番ステージ: distroless(シェルなし・最小CVE) FROM gcr.io/distroless/python3-debian12 COPY --from=builder /opt/venv /opt/venv COPY --from=builder /app /app ENV PATH="/opt/venv/bin:$PATH" WORKDIR /app CMD ["app.py"]
2. デバッグ時はdebugタグを使う
DistrolessにはBusyBoxシェル付きの:debugタグが用意されています。本番では:latest(またはバージョン固定)を使い、問題発生時は一時的に:debugタグのイメージに切り替えてデバッグします。# 通常本番用: シェルなし FROM gcr.io/distroless/python3-debian12 # デバッグ時のみ: busyboxシェルあり(本番には戻すこと) FROM gcr.io/distroless/python3-debian12:debug # debugタグのコンテナに入る方法 $ docker exec -it mycontainer /busybox/sh
3. 観測性の事前設計
シェルなし運用ではログをstdout/stderrに集約し、docker logsやFluentd等で外部収集することが前提になります。コンテナ内のファイルに書き込んでdocker execで取り出す運用は、Distrolessでは設計上選べません。アプリ側のログ設計(structlogやjson形式のstdout出力)をDockerfile設計と同時に決めておきましょう。マルチステージビルドとベースイメージ選定の組み合わせ
ベースイメージの選択を語る上で、マルチステージビルドとの組み合わせは避けて通れません。典型的なパターンは「ビルドステージに重いイメージ、リリースステージに軽いイメージ」を使うことです。Goで書いたアプリを例にすると、ビルドステージでは
golang:1.23-bookwormを使いますが、最終成果物は静的バイナリ1つなので、リリースステージにscratchやgcr.io/distroless/static-debian12を使えます。コンテナを本番環境に安全に展開するための設計全体を体系的に学びたい方には、Dockerハンズオン講座でコンテナ設計の「型」を習得する方法もあります。
# Goアプリの典型的なマルチステージビルド FROM golang:1.23-bookworm AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . # 静的ビルド(CGO無効化 + 外部ライブラリなし) RUN CGO_ENABLED=0 GOOS=linux go build -o /app ./cmd/server # リリースステージ: distroless static(CVEほぼゼロ) FROM gcr.io/distroless/static-debian12 COPY --from=builder /app /app ENTRYPOINT ["/app"]
distroless/python3やdistroless/java21を選びます。ビルドステージとリリースステージで異なるベースイメージを組み合わせるのがDockerfile設計の基本です。ビルドステージの重いイメージはCIビルド時にしか使われないため、最終イメージには含まれません。つまり「ビルド環境の充実度」と「本番イメージの軽量さ・安全性」を両立できるのがマルチステージビルドの強みです。
ベースイメージのバージョン固定とタグ設計
FROM debian:latestのように:latestタグを使うと、ビルドする時期によって異なるバージョンのイメージが取得されます。開発環境では動いていたのに、1ヶ月後にCI/CDでビルドすると壊れた──というトラブルの典型的な原因です。1. バージョンタグを必ず固定する
# NG: latestは使わない FROM debian:latest # OK: バージョンを固定 FROM debian:12.7-slim # 言語ランタイムのイメージも同様に固定 FROM python:3.12.7-slim-bookworm FROM golang:1.23.2-bookworm FROM node:22.11.0-bookworm-slim
2. 厳密な再現性が必要ならダイジェスト固定
バージョンタグは同じタグに異なるイメージが上書きされる(タグの再利用)可能性がゼロではありません。ビルドの完全な再現性を担保したい場合は、イメージのダイジェスト(SHA256ハッシュ)で固定します。# ダイジェスト固定(これより厳密な固定はない) FROM debian:12-slim@sha256:a1b2c3d4e5f6...(実際のハッシュ値に置き換える) # ダイジェストの確認方法 $ docker pull debian:12-slim $ docker inspect --format='{{index .RepoDigests 0}}' debian:12-slim debian@sha256:a1b2c3d4e5f6...
用途別ベースイメージ選定の判断フロー
ここまでの内容を整理して、アプリ種別ごとの推奨ベースイメージをまとめます。| アプリの種類 | ビルドステージ | リリースステージ | 理由・注意点 |
|---|---|---|---|
| Goバイナリ(静的ビルド) | golang:1.23-bookworm | gcr.io/distroless/static-debian12 または scratch | 静的バイナリのみ必要。CVE最小化できる |
| Pythonアプリ(科学系・pip依存多) | python:3.12-slim-bookworm | python:3.12-slim-bookworm | manylinuxホイール依存でglibc必須。alpineはNG |
| Pythonアプリ(標準ライブラリ中心) | python:3.12-slim-bookworm | gcr.io/distroless/python3-debian12 | ランタイム内包でCVE少・セキュリティ重視 |
| Node.jsアプリ | node:22-bookworm-slim | node:22-bookworm-slim | ネイティブ拡張があればalpineは危険 |
| Javaアプリ(JREのみ) | eclipse-temurin:21-jdk-noble | gcr.io/distroless/java21-debian12 | JREはglibc前提。distrolessで最小化 |
| シェルスクリプトツール | 不要(ステージ1本) | alpine:3.20 | busyboxで十分。musl問題なし |
debian:12-slimを選んでおくのが現場の経験則です。glibc互換性の問題もなく、apt経由でパッケージを追加でき、トラブルが少ない選択肢です。後からセキュリティ要件が厳しくなったタイミングでdistrolessへ移行するのが、無理のない段階的な改善です。トラブルシュート|よくあるエラーと対処法
1. 「exec format error」が出る
standard_init_linux.go:228: exec user process caused: exec format error
docker buildxでターゲットアーキテクチャを指定してリビルドしてください。# x86_64(linux/amd64)向けにクロスビルド $ docker buildx build --platform linux/amd64 -t myapp:latest .
2. distrolessにCOPYしたバイナリが起動しない
# distroless/staticにglibcバイナリをCOPYした場合の起動エラー $ docker run --rm myapp /app: not found # 原因確認: バイナリのglibc依存をlddで確認 $ ldd ./app linux-vdso.so.1 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 # glibc依存あり → distroless/staticはNG
gcr.io/distroless/static-debian12にglibcが必要なバイナリをCOPYすると起動に失敗します。glibcを含むgcr.io/distroless/base-debian12またはgcr.io/distroless/cc-debian12に変更してください。3. alpineでpip installが失敗する
# alpineでcryptographyインストール時のエラー例 ERROR: Could not build wheels for cryptography # 対処1: ビルドツールを追加(サイズが増大する) RUN apk add --no-cache gcc musl-dev libffi-dev openssl-dev python3-dev # 対処2(推奨): ベースイメージをdebian:12-slimに変更する FROM python:3.12-slim-bookworm
debian:12-slimに変更するほうが根本解決になります。4. ベースイメージを更新してもCVEが残る
docker pull debian:12-slimでイメージを更新しても、ビルドキャッシュが残っているため古いレイヤーが使われることがあります。# キャッシュを使わず完全再ビルド $ docker build --no-cache -t myapp:latest . # または最新イメージを明示的にpullして再ビルド $ docker build --pull -t myapp:latest .
本記事のまとめ
DockerfileのFROM命令でどのベースイメージを選ぶかは、「なんとなく動くから」で決めてよいものではありません。イメージサイズ・CVE数・デバッグ容易性という3つの軸で整理すると、用途ごとの最適解が見えてきます。| ベースイメージ | サイズ | CVE | 主な用途 |
|---|---|---|---|
| debian:12-slim | 中(~74MB) | 中 | Python/Node/Rubyアプリのデフォルト。apt使用可 |
| ubuntu:24.04 | 中(~78MB) | 中 | Ubuntuネイティブパッケージが必要な場合 |
| alpine:3.20 | 小(~7MB) | 少 | シェルツール・静的バイナリ。glibcアプリには不向き |
| distroless | 小~中 | 最少 | セキュリティ要件が厳しい本番環境。シェルなし |
| scratch | 0MB | ゼロ | 完全静的Goバイナリ。極限のサイズ削減 |
debian:12-slimを選んでおくと、glibc互換性の問題もなく、aptでパッケージを追加でき、トラブルの少ない選択肢になります。セキュリティスキャン(trivyなど)を定期的に実行し、CVEが増えたタイミングでベースイメージを更新する運用習慣を付けておくと、本番コンテナの健全性を長期的に維持できます。
DockerfileのFROM設計・マルチステージビルド・Composeを組み合わせたコンテナ設計の実践を、ハンズオン形式で学べる講座を開催しています。
Docker実践講座の詳細を見る>>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:Docker Composeのprofilesで開発・本番・デバッグ用サービスを切り替える方法|--profileオプションとCOMPOSE_PROFILESの実践設計
- この記事の属するカテゴリ:Dockerへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます