言語を切り替える
テーマを切り替える

Dockerfile最適化術:5つの秘訣でイメージサイズを80%削減する

午前3時。私は「Pushing to registry」と表示されたまま30分も動かない端末のプログレスバーを見つめていました。

3.2GB。

初めて作ったNode.jsアプリのDockerイメージは、ネットのチュートリアル通りに作ったはずなのに、信じられないほど肥大化していました。翌朝、同僚からSlackで「君のイメージ、OSまるごと入ってるんじゃない?僕のノートPCの容量がヤバいんだけど」と苦情が来ました。

正直、何が悪いのか分かりませんでした。Ubuntuベースだから? node_modules? それともコンパイルツール? 結果として、シンプルなAPIサーバーなのに、コード本体の50倍ものサイズになっていたのです。

その後、数日かけてDocker公式ドキュメントとベストプラクティスを研究し、この3.2GBの怪物を180MBまでダイエットさせることに成功しました。実に94%の削減です。

180MB
最適化後のサイズ

この記事では、その過程で学んだ「最も効果的だった5つのテクニック」を紹介します。単なるコマンドの羅列ではなく、「なぜそうするのか」という原理も解説します。原理さえ分かれば、どんな言語やフレームワークにも応用できるからです。

なぜDockerイメージはそんなに巨大になるのか

テクニックの前に、敵(サイズ肥大化)の正体を知りましょう。

Dockerイメージは「レイヤー(層)」構造をしています。RUNCOPYADD という命令を書くたびに、新しいファイルシステムの層が重なっていきます。重要なのは、「層は追加されるだけで、削除されない」 という点です。

例えば、Dockerfileでこう書いたとします:

RUN apt-get update
RUN apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*

一見、最後でキャッシュを消しているので綺麗に見えます。しかし実際には、2行目でインストールされたキャッシュデータは「2層目」に永久保存されています。3行目は「ファイルが見えなくなった」という情報を記録しているだけで、データ自体はイメージの中に残ったままなのです。

これは、部屋の掃除をするたびに「掃除前の部屋の写真」を保存しているようなものです。ゴミを捨てても、ゴミが写った写真は残るので、アルバム(イメージ)の厚さは変わりません。

また、基礎イメージの選択も重要です。ubuntu:20.04 はそれだけで72MB、node:16 に至っては1.09GBもあります(Debianのフルセットが入っているからです)。

これを踏まえて、最適化の戦略は3つです:

  1. レイヤー(層)を減らす。
  2. 軽量な基礎イメージを選ぶ。
  3. インストールと掃除を「同じ層」で終わらせる。

テクニック1:基礎イメージ選びで勝負は決まる

家を建てる時の土地選びと同じです。最初に何を選ぶかで、最終的なサイズの下限が決まります。

数字で比較してみましょう:

  • node:16 → 1.09GB
  • node:16-slim → 240MB
  • node:16-alpine → 174MB
  • alpine:latest → 5.6MB

一目瞭然です。私は node:16 から node:16-alpine に変えただけで、コードを一行も変えずに1.2GBから400MBまで減らすことができました。

Alpine Linuxとは?
コンテナのために設計された、セキュリティ重視の超軽量Linuxディストリビューションです。標準的なglibcの代わりにmusl libcを、aptの代わりにapkを使用します。

Alpineの互換性の罠
ただし注意点があります。musl libcを使用しているため、C/C++で書かれた一部のライブラリ(ネイティブ拡張)が動かないことがあります。以前、あるNode.jsの画像処理ライブラリが動かず苦労しました。

推奨戦略:

  1. 基本は Alpine版-alpine)を試す。
  2. 互換性エラーが出たら Slim版-slim、Debianベースの軽量版)を使う。
  3. それでもダメなら標準版(ごく稀です)。
# 変更前
FROM node:16

# 変更後
FROM node:16-alpine

たったこれだけで800MB削減です。

テクニック2:RUNコマンドを結合する

「削除は同じ層で行う」。これを実現するために、関連するコマンドを && で繋いで、1つの RUN 命令にまとめます。

悪い例(3層作られる)

RUN apt-get update
RUN apt-get install -y python3 gcc
RUN rm -rf /var/lib/apt/lists/*

これだと、2層目でキャッシュが保存され、3層目で消しても手遅れです。

良い例(1層で完結)

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

これなら、インストールして掃除まで終わった状態だけが1つの層として保存されます。

バックスラッシュ \ の活用
長いコマンドは \ で改行して可読性を高めましょう。

何を結合すべき?

  • 結合すべき:インストール+掃除、ダウンロード+解凍+削除。
  • 結合しないnpm installnpm run build など、キャッシュを活用したい工程は分けた方がビルド時間が短縮できます。

私のプロジェクトでは、12個あったRUN命令を4個にまとめただけで、520MBから320MBまで縮みました。

テクニック3:マルチステージビルド(Multi-stage Build)

これぞ最強のダイエット術です。

GoやTypescript、C++などのコンパイルが必要な言語では、「ビルド環境」にはコンパイラやヘッダーファイルが必要ですが、「実行環境」にはコンパイル済みのバイナリだけあれば十分です。ビルド道具まで本番イメージに入れるのは無駄です。

マルチステージビルドを使えば、「ビルド用のイメージ」で作った成果物だけを、「実行用のイメージ」にコピーして持っていくことができます。

Node.js(TypeScript)の例

# === 第1ステージ:ビルド ===
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build  # TypeScriptをJSにコンパイル

# === 第2ステージ:実行 ===
FROM node:16-alpine
WORKDIR /app
# ビルド済みのファイルだけコピー
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

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

COPY --from=builder が魔法の言葉です。第1ステージの重たい中間ファイル(TSソースコード、キャッシュなど)は全て破棄され、綺麗な第2ステージだけがイメージとして残ります。

私の場合、TypeScriptのソースコードと開発依存パッケージ(devDependencies)を含めた400MBが、マルチステージビルドで220MBになりました。

さらに一歩進んで、実行ステージでは npm install --production で本番用パッケージだけ入れると、さらに軽くなります。

テクニック4:.dockerignore で無駄を排除

.dockerignore.gitignore のDocker版です。

COPY . . を実行した時、指定したファイルを除外してくれます。これがないと、巨大な node_modules.git ディレクトリ(履歴データ)、ローカルのログファイル、そして最悪の場合 .env(秘密鍵)までイメージにコピーされてしまいます。

プロジェクトルートに .dockerignore を作りましょう:

node_modules
npm-debug.log
.git
.gitignore
.env
.DS_Store
coverage/
dist/

特に .git フォルダは見落としがちですが、長く続いているプロジェクトだと数百MBになっていることもあります。これを設定しただけで、ビルド時間が2分から30秒に短縮されました。

テクニック5:パッケージマネージャのキャッシュ削除

npm、pip、apt、apkなどは、次回インストールを速くするためにダウンロードしたファイルをキャッシュします。しかし、Dockerイメージにおいては「次」はないので、このキャッシュはただのゴミです。

各ツールごとの正しい掃除方法:

Alpine (apk)

RUN apk add --no-cache python3
# --no-cache オプションでそもそもキャッシュを作らせない

Debian/Ubuntu (apt)

RUN apt-get update && apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

Node.js (npm)

RUN npm install && npm cache clean --force

Python (pip)

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

実測データでは、Pythonプジェクトで --no-cache-dir をつけるだけで140MBも軽くなりました。

究極の最適化ケーススタディ

最後に、私のNode.js APIサーバーがどう変化したか、全貌をお見せします。

初期状態(1.2GB)

FROM node:16
COPY . .
RUN npm install
CMD ["node", "index.js"]

シンプルですが、Debian全入り+開発依存ファイル+キャッシュ+ソースコード全部入りです。

最適化適用後(180MB)

  1. node:16-alpine に変更(-800MB)
  2. .dockerignore 追加(-20MB)
  3. マルチステージビルド導入(-200MB)
  4. 本番依存のみインストール+キャッシュ削除(-40MB)

最終的なDockerfile

# Build Stage
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Production Stage
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
# 本番依存のみ install し、キャッシュを掃除
RUN npm install --production && npm cache clean --force
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

結果:1.2GB → 180MB(85%削減)

まとめ

Dockerイメージの最適化は、以下の5ステップで進めてください:

  1. Alpineを使う:まず土台を変えるのが一番効きます。
  2. マルチステージビルド:ビルド道具を実行環境に持ち込まない。
  3. RUNをまとめる:インストールと掃除はセットで。
  4. キャッシュを消す--no-cache などを活用。
  5. 余計なものを入れない.dockerignore を忘れずに。

最初は面倒に感じるかもしれませんが、一度テンプレートを作ってしまえば、あとは使い回すだけです。イメージが軽くなれば、デプロイは速くなり、ストレージコストは下がり、セキュリティリスクも減ります(不要なソフトが入っていないため)。

ぜひ、あなたの手元のDockerfileを見直してみてください。「えっ、こんなに減るの?」という驚きが待っているはずです。

Dockerイメージ軽量化・完全フロー

5つのテクニックを組み合わせて、3.2GBのイメージを180MBまで94%削減する手順

⏱️ Estimated time: 1 hr

  1. 1

    Step1: テクニック1:ベースイメージをAlpineに変更

    FROM node:16 を FROM node:16-alpine に変更するだけで、約800MB削減できます。
    注意点:Alpineはmusl libcを使用しているため、稀にC++ネイティブ拡張が動かないことがあります。その場合は -slim タグ(Debian精量版)を試してください。
  2. 2

    Step2: テクニック2:RUNコマンドの結合と掃除

    apt-get install と rm -rf /var/lib/apt/lists/* を && で繋ぎ、同じRUN命令内で実行します。
    これにより、一時ファイルがレイヤーに残るのを防げます。
    例:RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
  3. 3

    Step3: テクニック3:マルチステージビルド

    ビルド環境(SDK、コンパイラ)と実行環境(ランタイム)を分けます。
    AS builder でビルドし、COPY --from=builder で成果物だけを軽量な実行用イメージにコピーします。
    これにより、ソースコードやビルドツールを最終イメージから排除できます。
  4. 4

    Step4: テクニック4:不要ファイルの除外

    .dockerignore ファイルを作成し、node_modules, .git, .env などを除外します。
    ビルドコンテキストの転送時間が短縮され、意図しないファイルの混入を防げます。

FAQ

なぜRUNコマンドをまとめる必要があるのですか?
Dockerイメージはレイヤー(層)構造で、一度作られたレイヤーの内容は後のレイヤーで削除しても物理的には消えません(見えなくなるだけです)。そのため、インストール(ファイル追加)とキャッシュ削除(ファイル削除)を同じRUNコマンド(同じレイヤー)内で完結させることで、実際にディスク容量を節約できます。
Alpineイメージを使ってエラーが出た場合は?
Alpineは軽量化のためにmusl libcを使用しており、一部のライブラリと互換性がありません。その場合は、`bash`が入っていて互換性が高いがサイズも小さめの `slim` 系タグ(例:python:3.9-slim, node:16-slim)を使用するのがベストプラクティスです。
マルチステージビルドのメリットは何ですか?
開発ツール(コンパイラ、ヘッダーファイルなど)を最終的な本番イメージに含めなくて済むことです。例えばGo言語の場合、数百MBあるGoコンパイラを使ってビルドし、最終イメージには数MBの実行バイナリだけを入れることができ、劇的なサイズダウンが可能です。
npm installの最適化方法は?
本番環境用イメージでは `npm install --production` を使い、開発用依存パッケージ(devDependencies)をインストールしないようにします。また、`npm cache clean --force` を併用してキャッシュデータを削除することで、さらに数十MB削減できます。

4 min read · 公開日: 2025年12月17日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事