Dockerfileの書き方と軽量化のコツ

コンテナ化技術がクラウドネイティブ開発の標準となった現在、Dockerfileを適切に書くスキルは、すべてのインフラエンジニアと開発者にとって必須の知識です。しかし、何の戦略もなくDockerfileを書くと、イメージサイズが肥大化し、ビルド時間が急増し、セキュリティリスクが高まる危険性があります。本記事では、初心者から実務レベルまで、段階的にDockerfileの最適化テクニックを解説します。

結論:Dockerfileの最適化は「マルチステージビルド」「レイヤーキャッシュの戦略的配置」「不要ファイルの徹底削除」の3つを組み合わせることで実現できます。これらを適切に実装することで、イメージサイズを30~80%削減でき、ビルド時間を数分から数十秒に短縮し、本番環境のセキュリティリスクを大幅に低減できるとされています(出典:Docker公式ドキュメント、CNCF Cloud Native Survey 2024)。実務で即座に活用できる実装例を交えながら、プロジェクトに適用可能な知識をお届けします。

この記事は約12分で読めます。

目次

  1. Dockerfileの基本構造を理解する
  2. イメージ軽量化のベストプラクティス
  3. セキュリティと実装の最適化
  4. 実務で避けるべき落とし穴
  5. 実装例とサイズ比較
  6. よくある質問(FAQ)
  7. まとめ

Dockerfileの基本構造を理解する

Dockerfileの効率化を考える前に、Dockerの基本的な仕組みを理解することが重要です。多くの開発者がDockerfileを作成する際に陥りやすい落とし穴は、イメージレイヤーの概念を見落とすことです。正しくレイヤー構造を設計することで、ビルドキャッシュが有効に機能し、開発効率とデプロイ速度が大幅に向上するとされています。

Dockerfileとは何か

Dockerfileは、Dockerコンテナイメージを構築するための命令書です。テキストファイル形式で、一連の命令を記述することで、アプリケーションの実行環境をコード化できます。これにより、開発環境と本番環境の一貫性が保証され、「私のマシンでは動作するが、本番環境では動かない」といった環境による不具合を根本的に解決できるとされています。

Dockerfileから生成されるのがイメージです。イメージは、アプリケーションとその依存関係をすべて含む、ファイルシステムのスナップショットと考えるとわかりやすいでしょう。イメージは積み重なった複数のレイヤー(層)で構成されており、各レイヤーは前のレイヤーの変更を差分で保持しています。このレイヤー構造が、後述する「キャッシュ」の仕組みとなり、開発効率に直結する重要な概念です。

イメージレイヤーの仕組みを理解する

Dockerイメージは、複数のレイヤー(層)が積み重なった構造になっています。Dockerfile内の各コマンド(FROM、RUN、COPY、ADD等)は1つのレイヤーを作成します。コンテナ実行時には、これらのレイヤーが読み込み専用で積み重ねられ、最上部に書き込み可能なレイヤーが追加されます。

ビルドキャッシュの効率性を最大化するためには、変更頻度が低いコマンドを先に配置し、頻繁に変わる内容を後に配置することが重要です。例えば、ベースイメージの指定やシステムパッケージのインストールは変わらないため先頭に配置し、アプリケーションコードのCOPYやビルドコマンドは後に配置することで、開発中のビルド時間を短縮できます。

Docker Official Images は SBOM(Software Bill of Materials)を公開しており、セキュリティスキャンを実施しているとされています(出典:Docker Hub)。信頼性の高いベースイメージを選択することで、既知の脆弱性リスクを低減できます。

Dockerfileの主要な命令と役割

Dockerfileで頻出する命令を、表で整理します。各命令の目的と使用方法を理解することで、より効率的で安全なDockerfileを設計できます。

命令役割使用例レイヤー生成
FROMベースイメージを指定FROM python:3.11-slimはい
RUNシェルコマンドを実行RUN apt-get update && apt-get install -y curlはい
COPYホスト側ファイルをコンテナにコピーCOPY . /appはい
ADDファイルをコピー(tar解凍可、通常COPY推奨)ADD archive.tar.gz /appはい
WORKDIR作業ディレクトリを設定WORKDIR /appいいえ
ENV環境変数を設定ENV DEBUG=trueはい
EXPOSE公開ポートを宣言(実際には公開しない)EXPOSE 8080いいえ
CMDデフォルトコマンド(上書き可能)CMD ["python", "app.py"]いいえ
ENTRYPOINTエントリーポイント(上書き困難)ENTRYPOINT ["/usr/bin/app"]いいえ
USER実行ユーザーを変更USER appuserいいえ

レイヤー生成の有無が重要です。新しいレイヤーを生成する命令が増えるほど、イメージサイズが増加し、ビルドキャッシュの管理が複雑になります。後述する「キャッシュ戦略」では、この命令の順序と組み合わせが最適化の鍵となります。

イメージ軽量化のベストプラクティス

マルチステージビルドで不要なファイルを排除

マルチステージビルドは、1つのDockerfileに複数のFROMコマンドを記述し、異なるビルド段階を定義する手法です。典型的な用途は、「ビルド環境」と「実行環境」を分離することです。例えば、Go言語やNode.jsで開発したアプリケーションをビルドする場合、ビルドに必要なコンパイラやビルドツールが含まれたレイヤーでコンパイルを実行し、その後、生成されたバイナリだけを軽量な実行環境にコピーします。この手法により、イメージサイズを30~80%削減できるとされています(出典:Docker公式ドキュメント)。

マルチステージビルドを採用すると、以下のメリットが得られます:

  • セキュリティリスク低減:ビルドツールが本番環境に含まれず、攻撃面を最小化できます
  • イメージサイズ削減:ディスク容量が削減され、ネットワーク転送時間も短縮されます
  • キャッシュ効率向上:各ステージが独立してキャッシュされるため、ビルド時間が短縮されます
  • 本番環境の簡潔性:不要な開発ツールが除外され、予測可能で再現性の高いイメージになります

実務では、Go、Java、C++、Rust、TypeScriptなど、ほぼすべてのコンパイル言語アプリケーションに対してマルチステージビルドを適用することが推奨されています。

具体例:Go言語アプリケーションの場合

FROM golang:1.21 AS builder

WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp

FROM gcr.io/distroless/base:nonroot

COPY --from=builder /src/myapp /app/myapp
EXPOSE 8080
CMD ["/app/myapp"]

ビルドステージでは、Go SDK(数百MB)をインストールし、アプリケーションを静的にコンパイルします。ランタイムステージでは、最小限のベースイメージ(distroless、わずか約20MB)を使用し、コンパイル済みバイナリのみをコピーしています。結果として、最終イメージはビルドツール一式を含まず、ビルドステージなしの場合と比べて約90%軽量化されるとされています。

レイヤーキャッシュを最大活用する

Dockerのビルドプロセスでは、各命令がレイヤーを生成し、前のビルドのレイヤーをキャッシュとして再利用しようとします。キャッシュが有効に働くと、ビルド時間は数秒で済みますが、キャッシュが無効化されると全レイヤーが再構築されるため、数分単位の時間がかかります。

キャッシュを無効化させない工夫をいくつか紹介します。

① 変更頻度の低い命令を先に配置する

# 良い例(キャッシュ効率が高い)
FROM python:3.11-slim
RUN apt-get update && apt-get install -y python3-dev build-essential
COPY requirements.txt /app/
RUN pip install -r requirements.txt
WORKDIR /app
COPY . /app
CMD ["python", "app.py"]

# 悪い例(アプリケーションコード変更時に全て再構築)
FROM python:3.11-slim
WORKDIR /app
COPY . /app
RUN apt-get update && apt-get install -y python3-dev build-essential
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

良い例では、依存関係ファイル(requirements.txt)は変更頻度が低いため、先に配置します。アプリケーションコードの変更時にも、依存関係のインストールレイヤーはキャッシュが有効に働くため、ビルド時間を数秒に短縮できます。悪い例では、COPY . /appでアプリケーション全体をコピーするため、コード変更があるたびに依存関係の再インストールが強制されます。

② RUN命令を適切に統合する

# 非効率な例(3つのレイヤーが生成される、約50MB増加)
RUN apt-get update
RUN apt-get install -y curl wget git
RUN rm -rf /var/lib/apt/lists/*

# 効率的な例(1つのレイヤーで完結、キャッシュも最小化)
RUN apt-get update && \
    apt-get install -y curl wget git && \
    rm -rf /var/lib/apt/lists/*

複数のRUN命令を1つにまとめ、&&で連結することで、レイヤー数を削減できます。特にAPT(Debian/Ubuntu)のパッケージマネージャを使う場合、キャッシュの削除(rm -rf /var/lib/apt/lists/*)を同じレイヤーで行うことが重要です。分離すると、キャッシュファイルが別レイヤーに残り、イメージサイズが数十MB増加する可能性があります。

不要ファイルを徹底削除する

.dockerignoreファイルを活用して、ホスト側から不要なファイルがコンテナにコピーされるのを防ぎましょう。

.git
.gitignore
.dockerignore
README.md
.DS_Store
node_modules
__pycache__
.venv
.pytest_cache
.coverage
*.log
tmp/
dist/
build/
.env
.env.local

これにより、ビルドコンテキスト(ホストからDockerデーモンに送信されるファイル群)のサイズが減少し、ビルド速度が向上します。特にNode.jsプロジェクトでは、node_modulesを除外することで、送信量を数百MB削減できるケースが多いとされています(出典:CNCF Cloud Native Survey 2024)。

セキュリティと実装の最適化

最小限のベースイメージを選択

ベースイメージの選択は、最終的なイメージサイズ、セキュリティリスク、実行速度に大きな影響を与えます。一般的な選択肢の比較を見てみましょう。

イメージサイズ目安特徴推奨用途
python:3.11約900MBフル機能、開発ツール多数開発・テスト環境
python:3.11-slim約150MB最小限、コンパイルツール除外本番環境推奨
python:3.11-alpine約50MB極小、musl libc使用軽量優先(ただしC拡張注意)
distroless/python3約100MBセキュリティ最優先、システムツール無し本番環境・セキュリティ重視

-slimまたはdistrolessイメージを選択することで、不要なシステムツールやライブラリが除外され、攻撃面(attack surface)を減らせるとされています(出典:OWASP、Docker公式セキュリティガイドライン)。

複数のRUN命令を統合してレイヤー削減

既出の内容に関連しますが、レイヤーの数を最小化することは、イメージサイズ削減だけでなく、イメージの管理性向上にも寄与します。

# ビルド・実行用の統合例(Node.js)
FROM node:18-alpine AS builder

WORKDIR /build
COPY package*.json ./
RUN npm ci && npm run build

FROM node:18-alpine

WORKDIR /app
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/node_modules ./node_modules
COPY package*.json ./

ENV NODE_ENV=production
EXPOSE 3000

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

このDockerfileでは、ビルドステージで依存関係とバンドル処理を完結させ、ランタイムステージには必要な成果物のみをコピーしています。

実務で避けるべき落とし穴

ユーザー権限の設定ミス

デフォルトではコンテナはrootユーザーで実行されます。これはセキュリティリスクであるため、非rootユーザーの作成が推奨されています。

# セキュアな例
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .

# 非rootユーザーを作成・切り替え
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

USER nodejs

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

コンテナがブレイクアウト(逃脱)されたとしても、非rootユーザーの権限しか得られないため、ホスト側への影響を最小化できるとされています。

明示的なバージョン指定の省略

バージョンを指定しないと、latestタグが自動選択されます。

# 危険:latestはいつ変わるか不定
FROM python:latest

# 推奨:メジャーバージョンを明示
FROM python:3.11

# さらに厳密:マイナーバージョンも指定
FROM python:3.11.5

バージョンを固定することで、意図しない機能変更や非互換な更新を防ぎ、イメージの再現性を確保できます。

キャッシュ無効化命令の不適切な配置

タイムスタンプやランダム値を含む命令(RUN dateなど)をDockerfileに含めると、毎回キャッシュが無効化されます。

# 避けるべき
RUN apt-get update && apt-get install -y $(date +%s) packages

# 推奨:buildのたびに再実行したい場合は、--no-cache フラグを使用
# docker build --no-cache .

必要に応じて、コマンドラインで--no-cacheフラグを指定する方が、意図が明確になります。

環境変数と設定の混在

機密情報(APIキーなど)をDockerfileに埋め込んではいけません。ランタイムに注入するか、外部の設定管理ツール(Kubernetes Secrets など)で管理するべきです。

# 絶対禁止
ENV DATABASE_PASSWORD="secret123"

# 推奨:実行時に環境変数として指定
# docker run -e DATABASE_PASSWORD=secret123 myapp

実装例とサイズ比較

様々なベースイメージとビルド手法での比較

様々なベースイメージとビルド手法でのイメージサイズの比較を表で示します。これは同じアプリケーション(Pythonウェブアプリケーション)を異なるDockerfile設定でビルドした場合のおおよその参考値です。

構成方法ベースイメージビルド手法最終サイズメリット・デメリット
標準構成python:3.12シングルステージ約1.2GBシンプルだが、ビルドツールが含まれ容量が大きい
slim + シングルステージpython:3.12-slimシングルステージ約400MB改善されるが、開発用ツール依存で容量増加の可能性
alpine + シングルステージpython:3.12-alpineシングルステージ約200MB軽量だが、glibcライブラリの互換性に注意必要
slim + マルチステージpython:3.12-slimマルチステージ約250MBビルドツール除外で大幅削減。推奨構成
alpine + マルチステージpython:3.12-alpineマルチステージ約150MB最小サイズ。ただし互換性確認が必須

表から分かる通り、マルチステージビルドを採用することで、同じベースイメージの場合でも30~40%程度のサイズ削減が期待できるとされています。ベースイメージの選択とマルチステージビルドの組み合わせで、シングルステージの標準構成と比べて80%以上の削減も可能です。

実践的なDockerfile例

理論だけでなく、実際の業務で使えるDockerfile例を言語別に紹介します。

Node.js アプリケーション

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:22-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
CMD ["node", "index.js"]

Node.jsアプリケーションの場合、node_modulesがイメージサイズを大きくするため、マルチステージビルドが特に効果的です。このDockerfileの主な特徴は、npm ciを使用して依存関係をロック確実にインストールしていることや、非rootユーザー(nodejs)を作成してセキュリティを向上させていることが挙げられます。

Python アプリケーション

FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends build-essential && apt-get clean && rm -rf /var/lib/apt/lists/*
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.12-slim
WORKDIR /app
RUN useradd -m -u 1001 appuser
COPY --from=builder --chown=appuser:appuser /opt/venv /opt/venv
COPY --chown=appuser:appuser . .
ENV PATH="/opt/venv/bin:$PATH"
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]

Python向けDockerfileの最適化ポイントは、python:3.12-slimを使用して不要な開発ツールを除外していることや、ビルド段階でのみbuild-essentialをインストールして、実行段階では削除されるため、最終イメージサイズが大幅に削減されることが挙げられます。

よくある質問(FAQ)

Q1: RUN と CMD の違いは何ですか?

A: RUNはビルド時に実行され、新しいレイヤーを生成します。一方CMDはコンテナ起動時に実行されます。RUNで依存関係をインストールし、CMDでアプリケーションを起動するという役割分担が標準的です。

Q2: マルチステージビルドは常に必須ですか?

A: 単純なスクリプト言語のアプリケーションなど、ビルドステージと実行ステージの区別が不要な場合もあります。ただし、Go、Java、C++ など、コンパイルが必要な言語では、マルチステージビルドによる恩恵が大きいとされています。

Q3: alpineベースイメージでライブラリ互換性エラーが出た場合の対処法は?

A: alpineはmusl libcを採用しており、一部のglibcベースの共有ライブラリが動作しないことがあります。その場合は、slim派生イメージ(debian:12-slimなど)への切り替えを検討してください。これはalpineの次に軽量で、互換性問題が少ないとされています。

Q4: イメージサイズはどこで確認できますか?

A: docker imagesコマンドで確認できます。SIZE列に表示されます。詳細にはdocker inspectでメタデータを確認することも可能です。

Q5: ローカル開発と本番環境で異なるDockerfileを書くべきですか?

A: 条件に応じて、1つのDockerfile内でマルチステージビルドを活用し、ビルド引数で条件分岐させる方法があります。または、docker-compose.dev.yml と本番用の設定を分けるアプローチもあります。いずれにせよ、本番環境には不要な開発ツール・キャッシュを含めないのが原則です。

Q6: レジストリへのプッシュが遅い場合、何が原因ですか?

A: イメージサイズが大きい、または不要なレイヤーが多い可能性があります。.dockerignoreの見直し、不要なファイルの削除、ベースイメージの軽量化を検討してください。

まとめ

Dockerfileの書き方と軽量化は、単なる技術的な効率化ではなく、本番環境の安定性とセキュリティ、開発チーム全体の生産性に直結する重要なスキルです。本記事で紹介した3つの核となる施策——マルチステージビルド、レイヤーキャッシュの最適化、不要ファイルの徹底的な削除——を組み合わせることで、ほぼすべてのDockerfile最適化の課題に対応できるとされています。

実装時のチェックリスト:

  • □ マルチステージビルドで本番に不要なファイルを除外している
  • □ 変更頻度の低い命令を先に配置し、キャッシュ効率を最大化している
  • □ .dockerignoreで不要なファイルを除外している
  • □ slim または distroless のベースイメージを使用している
  • □ 非rootユーザーで実行するよう設定している
  • □ バージョンを明示的に指定している
  • □ 機密情報をDockerfileに埋め込んでいない

これらを実践することで、セキュアで軽量、かつ高速にビルドできるDockerfileを実現できます。クラウドネイティブな開発を進める上で、Dockerfileの最適化は継続的な改善の対象です。定期的に見直し、チーム全体のベストプラクティスとして共有していくことをお勧めします。バージョンやプラットフォームによって仕様が異なる可能性があるため、本番環境への適用前に、必ず公式ドキュメント(Docker公式サイト、各言語の公式イメージドキュメント)で最新の推奨事項を確認してください。また、セキュリティ設定やビルド最適化は、プロジェクト固有の要件や制約に基づいて自己責任で実施するようお願いします。

Route Bloom編集部が運営する Infra Academy では、現役エンジニアによる技術的観点に基づき、情報の正確性・最新性を優先して執筆・監修しています。編集ポリシーはこちら

ABOUT ME
たから
サラリーマンをしながら開業して経営やってます。 今年、本業で独立・別事業を起業予定です。 ◆経験:IT講師/インフラエンジニア/PM/マネジメント/採用/運用・保守・構築・設計 ◆取得資格:CCNA/CCNP/LPIC-1/AZ-900/FE/サーティファイC言語 ◆サイドビジネス:アパレル事業/複数のWEBメディアを運営