Dockerfileの書き方と軽量化のコツ | Infra Academy

Dockerfileの書き方と軽量化のコツ
コンテナ化技術がクラウドネイティブ開発の標準となった現在、Dockerfileを適切に書くスキルは、すべてのインフラエンジニアと開発者にとって必須の知識です。しかし、何の戦略もなくDockerfileを書くと、イメージサイズが肥大化し、ビルド時間が急増し、セキュリティリスクが高まる危険性があります。本記事では、初心者から実務レベルまで、段階的にDockerfileの最適化テクニックを解説します。
結論:Dockerfileの最適化は「マルチステージビルド」「レイヤーキャッシュの戦略的配置」「不要ファイルの徹底削除」の3つを組み合わせることで実現できます。これらを適切に実装することで、イメージサイズを30~80%削減でき、ビルド時間を数分から数十秒に短縮し、本番環境のセキュリティリスクを大幅に低減できるとされています(出典:Docker公式ドキュメント、CNCF Cloud Native Survey 2024)。実務で即座に活用できる実装例を交えながら、プロジェクトに適用可能な知識をお届けします。
この記事は約12分で読めます。
目次
- Dockerfileの基本構造を理解する
- イメージ軽量化のベストプラクティス
- セキュリティと実装の最適化
- 実務で避けるべき落とし穴
- 実装例とサイズ比較
- よくある質問(FAQ)
- まとめ
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 では、現役エンジニアによる技術的観点に基づき、情報の正確性・最新性を優先して執筆・監修しています。編集ポリシーはこちら




