- Published on
Docker BuildKit & イメージレイヤー完全ガイド 2025: LLB、Cache Mount、Multi-Stage、OCI、ビルド最適化の徹底解説
- Authors

- Name
- Youngju Kim
- @fjvbn20031
はじめに: docker build の進化
10 年前と今
2015 年の Docker ビルド:
$ docker build -t myapp .
Sending build context to Docker daemon 45.2MB
Step 1/12 : FROM node:14
Step 2/12 : WORKDIR /app
Step 3/12 : COPY package.json .
...
- 順次実行: 1 レイヤーずつ順番に。
- ビルドコンテキスト転送: すべてのファイルを daemon へ。
- キャッシュ無効化: 1 ファイル変更で全体再ビルド。
- 毎回 5 〜 10 分。
2025 年の同じビルド (BuildKit):
$ DOCKER_BUILDKIT=1 docker build -t myapp .
[+] Building 12.3s (15/15) FINISHED
=> [internal] load build definition 0.0s
=> [internal] load metadata 0.3s
=> [build 1/4] FROM node:14 0.0s (cached)
=> [build 2/4] COPY package.json . 0.1s
=> [build 3/4] RUN --mount=type=cache... npm install 8.2s
...
- 並列実行: 独立ステップを同時に。
- Cache mount: npm/maven/pip を再利用。
- きめ細かなキャッシュ: 変更されたファイルのみ無効化。
- 数秒。
同じ Dockerfile で 10 倍以上高速。核心の違い: BuildKit。
この記事で扱う内容
- Docker イメージ構造: OCI spec。
- レイヤーと UnionFS: コンテナの基盤。
- Legacy builder vs BuildKit: 何が変わったか。
- LLB (Low-Level Builder): BuildKit の DSL。
- Multi-stage builds: 小さく、安全に。
- Cache strategies: Layer、mount、registry。
- Reproducible builds。
- セキュリティ: distroless、scanning、SBOM。
- 実践的な最適化テクニック。
1. OCI イメージフォーマット
Open Container Initiative
OCI (Open Container Initiative) は、コンテナの標準を定義する Linux Foundation プロジェクト。主要な 2 つの仕様:
- Runtime Spec: コンテナの実行方法。
- Image Spec: イメージフォーマット。
Docker、Podman、containerd、CRI-O はすべて OCI に準拠。相互運用可能。
OCI イメージの構造
OCI イメージは いくつかのファイルの集合:
manifest.json # イメージメタデータ
config.json # コンテナ設定
layers/ # ファイルシステムレイヤー
├── blob1.tar.gz
├── blob2.tar.gz
└── blob3.tar.gz
Manifest
Image manifest: イメージのすべての構成要素のリスト。
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:abc123...",
"size": 1234
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:def456...",
"size": 54321
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:ghi789...",
"size": 123456
}
]
}
各エントリは content-addressable: SHA-256 digest で識別される。
Image Config
{
"architecture": "amd64",
"os": "linux",
"config": {
"Env": ["PATH=/usr/local/sbin:..."],
"Cmd": ["node", "server.js"],
"WorkingDir": "/app",
"ExposedPorts": {
"3000/tcp": {}
}
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:aaa...",
"sha256:bbb..."
]
},
"history": [
{
"created": "2025-01-01T00:00:00Z",
"created_by": "/bin/sh -c #(nop) ADD file:... in /"
}
]
}
- rootfs.diff_ids: レイヤーの 展開後の ハッシュ。Manifest の digest とは異なる。
- history: 各レイヤーの作成履歴。
Content Addressable Storage
すべての blob はハッシュで識別される:
- Layer ファイル:
sha256:def456... - Config:
sha256:abc123... - Manifest: それ自体もハッシュ。
利点:
- 重複排除 (layer deduplication): 同じレイヤーは 1 度だけ保存。
- 完全性: ハッシュで検証。
- 不変性: 同じハッシュ = 同じ内容。
Image Index (Multi-arch)
複数プラットフォーム向けのイメージ:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:amd64...",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:arm64...",
"platform": {
"architecture": "arm64",
"os": "linux"
}
}
]
}
docker pull 時にホストプラットフォームに合った manifest が選択される。
2. レイヤーと UnionFS
レイヤーの本質
Docker イメージの 各レイヤーは tar アーカイブ。ファイルシステムの 変更 (delta) を含む。
例:
FROM alpine:3.19 # Layer 1: 基本 Alpine ファイルシステム
COPY app.js /app/ # Layer 2: /app/app.js を追加
RUN apk add nodejs # Layer 3: nodejs パッケージと依存
RUN chmod +x /app/start.sh # Layer 4: ファイル権限の変更
各レイヤーは 前の状態からの変更 のみ:
- Layer 1: Alpine の全ファイル。
- Layer 2:
/app/app.jsの 1 ファイル。 - Layer 3:
/usr/bin/node、ライブラリなど数百ファイル。 - Layer 4:
/app/start.shのメタデータのみ。
Union Filesystem
Union FS (OverlayFS、AUFS、Btrfs) は複数のレイヤーを 1 つのビューに統合する:
merged view: /app/app.js, /usr/bin/node, ...
↑
OverlayFS
/ | \
Layer 1 Layer 2 Layer 3 (read-only)
\
container writable layer (read-write)
動作:
- 読み込み: 上のレイヤーから検索。見つかれば返す。
- 書き込み: 最上位の writable レイヤーへ保存 (copy-on-write)。
- 削除: whiteout ファイルでマーク。
Copy-on-Write
ファイルを修正するとき:
- 元のレイヤーからファイルを読み込む。
- Writable レイヤーへコピー。
- コピーを修正。
- 元は 変更されない。
結果:
- 同じイメージを複数のコンテナが 共有。
- 各コンテナは 自身の変更分のみ 保持。
- 数百コンテナが同じイメージ → 1 度だけ保存。
Layer Sharing
重要な利点: 複数のイメージが レイヤーを共有。
Image A: ubuntu:22.04 + node + myapp
Image B: ubuntu:22.04 + node + other-app
Shared layers:
- ubuntu:22.04 (1 度だけ保存)
- node (1 度だけ保存)
Unique layers:
- myapp (A のみ)
- other-app (B のみ)
ディスク節約:
- 10 イメージが同じ base を共有 → 1 倍の base + 10 倍のアプリレイヤー。
- 数十 GB が数 GB に。
レイヤー数のトレードオフ
多いレイヤー:
- ビルドキャッシュがきめ細かい → ビルドが速い。
- Pull 時に並列ダウンロード可能。
- ただし、各レイヤーごとにメタデータのオーバーヘッド。
少ないレイヤー:
- 小さいイメージ。
- Pull がより効率的な場合がある。
- キャッシュの粒度が低い。
実践的推奨: 10 〜 20 レイヤー。少なすぎず、多すぎず。
レイヤーサイズの制限
技術的には 制限なし。ただし:
- 単一レイヤー > 10 GB: 問題発生の可能性。
- 全体イメージ > 5 GB: pull 時間の問題。
- ファイル数 > 100 万: パフォーマンス低下。
実践では 数 GB 以下、数万ファイル が適切。
3. Legacy Builder vs BuildKit
Legacy Docker Builder
2017 年以前の Docker ビルド方式:
Dockerfile → Docker Daemon → Sequential steps
問題点:
- Sequential のみ: 独立ステップも順次実行。
- Client-server アーキテクチャ: ファイルをすべて daemon に転送。
- モノリシック: ビルドグラフがない。
- 制限されたキャッシュ: layer 単位のみ。
- 拡張が難しい。
BuildKit の登場
2017 年 Docker 18.06 で公開。現在 Docker Desktop の標準。containerd、Podman、Kaniko など多くのツールで使用。
主要なイノベーション:
- Build Graph: ビルドを DAG として表現。
- 並列実行: 独立ステップを同時ビルド。
- Cache mount: ビルド中に使用するキャッシュディレクトリ。
- Secret mount: ビルド中にシークレットを使用 (イメージに残らない)。
- Multi-stage build の最適化: 必要な stage のみビルド。
- リモートキャッシュ: Registry をキャッシュとして使用。
- マルチプラットフォーム: 複数 arch を一度に。
BuildKit の有効化
Docker 23+: デフォルトで有効。
明示的な有効化 (旧バージョン):
export DOCKER_BUILDKIT=1
docker build .
設定ファイル:
// ~/.docker/config.json
{
"features": {
"buildkit": true
}
}
パフォーマンスの違い
同じ Dockerfile の実測値:
| ビルド | Legacy | BuildKit |
|---|---|---|
| Cold (キャッシュなし) | 180s | 120s |
| Warm (1 ファイル変更) | 180s | 15s |
| Warm (独立ステップ) | 60s | 5s |
BuildKit の きめ細かなキャッシュ と 並列化 が生み出す差。
4. BuildKit の LLB
Low-Level Builder
LLB (Low-Level Builder) は BuildKit の内部表現:
Dockerfile → Parser → LLB → Executor → Image
LLB は ビルドグラフ を表現する低レベル DSL。Dockerfile は frontend の 1 つ。他の frontend も可能:
- Dockerfile frontend: デフォルト。
- Buildpack frontend: Cloud Native Buildpacks。
- カスタム frontend: 独自の DSL。
DAG ベースのビルド
BuildKit は Dockerfile を DAG に変換する:
FROM node:18 AS base
RUN apt-get update && apt-get install -y python3
FROM base AS deps
COPY package.json .
RUN npm install
FROM base AS build
COPY . .
RUN npm run build
FROM nginx:alpine AS runtime
COPY /app/dist /usr/share/nginx/html
DAG 表現:
node:18 (FROM)
│
▼
┌─── base ───┐
│ │
▼ ▼
deps build
│ │
▼ ▼
[unused] nginx:alpine (FROM)
│
▼
runtime
注目: deps stage は 最終イメージに含まれない。BuildKit はこれを検知して ビルドしない。Legacy builder は無条件にビルドする。
Parallelism
独立した stage は 並列実行:
FROM alpine AS a
RUN sleep 10
FROM alpine AS b
RUN sleep 10
FROM alpine
COPY /tmp /a
COPY /tmp /b
- Legacy: 20 秒 (10 + 10)。
- BuildKit: 10 秒 (並列)。
LLB の直接使用
Go コードで LLB を生成可能:
import "github.com/moby/buildkit/client/llb"
st := llb.Image("alpine:3.19").
Run(llb.Shlex("apk add --no-cache curl")).
Root()
def, err := st.Marshal(ctx)
// BuildKit へ送信
高度な使用例: Terraform や Bazel のようなツールが LLB をバックエンドとして使う。
5. Multi-Stage Builds
なぜ Multi-Stage か
問題: ビルドツールがイメージに残る。
# 悪い例
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
結果のイメージ: 〜 1 GB (Go コンパイラ、ソース、依存すべて含む)。
解決策: multi-stage build。
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Runtime stage
FROM alpine:3.19
COPY /app/myapp /myapp
CMD ["/myapp"]
結果のイメージ: 〜 10 MB (Alpine + バイナリのみ)。
100 倍削減。ビルドツールは builder stage にのみ存在し、最終イメージには含まれない。
COPY --from
他の stage や イメージ からファイルをコピー:
FROM alpine AS tools
RUN apk add --no-cache curl jq
FROM scratch
COPY /usr/bin/curl /usr/bin/curl
COPY /usr/bin/jq /usr/bin/jq
# 必要なバイナリのみ
FROM scratch
COPY /usr/local/bin/python3 /usr/local/bin/python3
# registry のイメージからもコピー可能
Stage の独立性
各 stage は 独立:
- 異なる base image。
- 異なるツール。
- 異なる目的。
ビルドの観点: BuildKit は stage 間の依存を検出。必要なものだけをビルド。
よくあるパターン
1. コンパイル言語 (Go、Rust、C):
FROM rust:1.75 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
COPY /app/target/release/myapp /usr/local/bin/
CMD ["myapp"]
2. Node.js:
FROM node:20 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20 AS build
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
3. Python:
FROM python:3.11 AS builder
COPY requirements.txt .
RUN pip install --target=/deps -r requirements.txt
FROM python:3.11-slim
COPY /deps /usr/local/lib/python3.11/site-packages
COPY . /app
WORKDIR /app
CMD ["python", "main.py"]
4. Java:
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre
COPY /app/target/*.jar /app.jar
CMD ["java", "-jar", "/app.jar"]
Stage の再利用
同じ stage を複数の場所から参照:
FROM alpine AS common
RUN apk add --no-cache ca-certificates
FROM common AS app1
# ...
FROM common AS app2
# ...
common stage は 1 度だけビルド され共有される。
6. Cache Strategies
Layer Cache (デフォルト)
Dockerfile の 順序がキャッシュに影響する:
FROM node:20
COPY . . # 全体コピー
RUN npm install # 毎回再実行
問題: ソースコードの 1 行だけでも変わると COPY . がキャッシュを無効化し、以降の npm install まで再実行。
正しい順序:
FROM node:20
COPY package*.json ./ # 依存ファイルのみ
RUN npm install # 依存のインストール
COPY . . # 残り (キャッシュと無関係)
原則: 頻繁に変わらないもの を上に、頻繁に変わるもの を下に。
.dockerignore
ビルドコンテキストから 除外するファイル:
node_modules
.git
*.log
.env
dist
coverage
効果:
- ビルドコンテキストのサイズ削減。
COPY .のキャッシュ安定性。- シークレット漏洩の防止。
Cache Mount (BuildKit の必須機能)
Cache mount は ビルド中のみ 使われるキャッシュディレクトリ。レイヤーには 含まれない。
# syntax=docker/dockerfile:1.6
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN \
npm install
COPY . .
RUN npm run build
動作:
/root/.npmは 永続キャッシュ。ビルド間で保持。- npm が既にダウンロードしたパッケージを再利用。
- イメージには含まれない。
パフォーマンスの違い:
- 初回ビルド: 60 秒。
- 次のビルド (キャッシュ活用): 5 秒。
言語別 Cache Mount の例
Node.js (npm):
RUN \
npm ci
Node.js (pnpm):
RUN \
pnpm install --frozen-lockfile
Python (pip):
RUN \
pip install -r requirements.txt
Go:
RUN \
go build -o /app ./...
Rust (cargo):
RUN \
cargo build --release
Java (Maven):
RUN \
mvn package
Registry キャッシュ
ビルドキャッシュを registry に保存 して他のマシンと共有:
docker buildx build \
--cache-to type=registry,ref=myregistry.com/myapp:cache \
--cache-from type=registry,ref=myregistry.com/myapp:cache \
-t myapp:latest .
CI/CD で有用:
- 各 CI マシンでキャッシュを共有。
- ビルド時間の大幅短縮。
Inline キャッシュ
イメージ自体にキャッシュメタデータを含める:
docker buildx build \
--cache-to type=inline \
--push \
-t myregistry.com/myapp:latest .
Pull したイメージをそのままキャッシュとして使用。
Git URL をキャッシュとして
リモートの Git リポジトリをキャッシュに:
# syntax=docker/dockerfile:1.6
FROM alpine
RUN \
...
7. Secret Management
問題: ビルド中のシークレット
# 悪い: トークンがイメージに残る
ARG GITHUB_TOKEN
RUN git clone https://$GITHUB_TOKEN@github.com/private/repo.git
結果:
docker historyにトークン露出。- イメージレイヤーに保存される。
- 永遠に残る。
Secret Mount
BuildKit の secret mount:
# syntax=docker/dockerfile:1.6
FROM alpine
RUN \
TOKEN=$(cat /run/secrets/github_token) && \
git clone https://$TOKEN@github.com/private/repo.git
ビルド時:
echo $GITHUB_TOKEN | docker buildx build \
--secret id=github_token,src=/dev/stdin \
-t myapp .
効果:
- シークレットは ビルド中のみ 使用。
- イメージに残らない。
- レイヤーにも存在しない。
SSH Mount
SSH agent socket をビルドへ渡す:
# syntax=docker/dockerfile:1.6
FROM alpine
RUN apk add --no-cache openssh-client git
RUN \
git clone git@github.com:private/repo.git
ビルド時:
docker buildx build --ssh default -t myapp .
SSH key はイメージに 一切含まれない。
8. Reproducible Builds
問題: 再現性のなさ
同じ Dockerfile、同じソース → 異なるイメージ になる?
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
理由:
apt-get update時点の apt リポジトリの状態。- 時間によってパッケージバージョンが変わる。
- ローカル環境の違い (ネットワーク、キャッシュ)。
結果: 昨日ビルドしたイメージと今日ビルドしたイメージが 異なる。セキュリティ監査、デバッグに問題。
Reproducible Build の条件
完全再現可能:
- 固定された base image:
FROM ubuntu:22.04@sha256:...(digest 固定)。 - 固定されたパッケージバージョン:
apt install curl=7.81.0-1ubuntu1.15。 - Lockfile の使用:
package-lock.json、requirements.txt(version pinning)。 - Deterministic ビルド: 時刻に依存しない。
- Clean environment: 外部依存を排除。
Digest Pinning
# 悪い: タグは移動可能
FROM node:20
# 良い: digest は不変
FROM node:20@sha256:8f0c5a7a1d0c7b8f...
Digest の確認:
docker pull node:20
docker images --digests | grep node
Package Pinning
apt (Debian/Ubuntu):
RUN apt-get update && apt-get install -y \
curl=7.81.0-1ubuntu1.15 \
&& rm -rf /var/lib/apt/lists/*
pip:
requests==2.31.0
flask==3.0.0
npm:
{
"dependencies": {
"express": "4.18.2"
}
}
package-lock.jsonをコミット。
SOURCE_DATE_EPOCH
Go、Rust などのコンパイラは ビルド時刻をバイナリに記録する。SOURCE_DATE_EPOCH で固定:
ARG SOURCE_DATE_EPOCH=0
FROM golang:1.21
ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH
RUN go build -ldflags="-buildid=" -trimpath .
検証
# 複数回ビルド
docker build -t myapp:1 .
docker build -t myapp:2 .
# イメージを比較
docker inspect myapp:1 | jq .RootFS.Layers
docker inspect myapp:2 | jq .RootFS.Layers
# 同一なら reproducible
実践的に: 100% 再現可能なビルドはまれ。ほぼ 再現可能で十分。
9. Image Security
Distroless Images
Google の distroless プロジェクト: 必要最小限のランタイムのみ含むミニマルイメージ。
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM gcr.io/distroless/base-debian12
COPY /app/myapp /
CMD ["/myapp"]
特徴:
- shell なし:
/bin/shなし。 - パッケージマネージャなし: apt、apk なし。
- 最小サイズ: 数十 MB。
- 攻撃面の最小化。
Scratch イメージ
Scratch: 完全に 空 のイメージ。
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
FROM scratch
COPY /app/myapp /
CMD ["/myapp"]
結果: バイナリ 1 つのみ。数 MB。
注意:
- 静的リンク必須 (
CGO_ENABLED=0)。 - デバッグが困難 (shell なし)。
- CA certificates が必要ならコピー。
Image Scanning
Trivy (Aqua Security):
trivy image myapp:latest
- CVE スキャン。
- Secret 検出。
- 設定ファイル分析。
Grype (Anchore):
grype myapp:latest
Snyk、Clair、Docker Scout など複数のツール。
SBOM (Software Bill of Materials)
イメージの すべてのソフトウェアのリスト:
生成:
syft myapp:latest -o spdx-json > sbom.json
内容:
- すべてのパッケージとバージョン。
- ライセンス情報。
- 依存ツリー。
活用:
- Supply chain セキュリティ。
- License compliance。
- CVE マッチング。
Image Signing
cosign でイメージに署名:
# 署名
cosign sign --key cosign.key myregistry.com/myapp:latest
# 検証
cosign verify --key cosign.pub myregistry.com/myapp:latest
Keyless signing (OIDC):
cosign sign myregistry.com/myapp:latest
# GitHub Actions OIDC による自動署名
Sigstore プロジェクトの一環。
10. 実践的な最適化
レイヤー最小化
悪い例:
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y jq
RUN rm -rf /var/lib/apt/lists/*
4 レイヤー。中間状態がレイヤーに残る。
良い例:
RUN apt-get update && \
apt-get install -y \
curl \
jq && \
rm -rf /var/lib/apt/lists/*
1 レイヤー。小さなイメージ。
一時ファイルの削除
同じレイヤー内で インストールとクリーンアップ:
RUN apt-get update && \
apt-get install -y build-essential && \
make && \
apt-get purge -y build-essential && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
.dockerignore の活用
# ビルドに不要
node_modules
.git
.github
*.log
*.md
# セキュリティリスク
.env
.env.local
**/secrets/
# 大容量の一時ファイル
dist
build
coverage
.next
効果:
- ビルドコンテキストのサイズ削減。
- キャッシュの安定性向上。
- セキュリティ向上。
小さい Base Image
# 悪い: 大きな base
FROM node:20 # 〜 1.1 GB
# 良い: slim
FROM node:20-slim # 〜 240 MB
# さらに良い: alpine
FROM node:20-alpine # 〜 180 MB
# 最高: distroless
FROM gcr.io/distroless/nodejs20-debian12 # 〜 180 MB、shell なし
レイヤー順序の最適化
原則: 不変 → 頻繁に変更 の順序で。
FROM node:20-alpine
# 1. システム依存 (ほぼ変わらない)
RUN apk add --no-cache libc6-compat
# 2. パッケージマネージャ設定 (ほぼ変わらない)
WORKDIR /app
COPY package.json package-lock.json ./
# 3. 依存のインストール (package.json 変更時のみ)
RUN \
npm ci --only=production
# 4. アプリケーションコード (頻繁に変更)
COPY . .
# 5. ビルド (コード変更時のみ)
RUN npm run build
CMD ["node", "dist/server.js"]
Multi-Platform Build
buildx で複数プラットフォームを一度に:
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry.com/myapp:latest \
--push .
結果: AMD64 と ARM64 イメージが 1 つの manifest list として push される。
BuildKit + CI/CD
GitHub Actions の例:
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myregistry.com/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
type=gha: GitHub Actions cache を使用。CI 間でキャッシュを共有。
11. よくある落とし穴
落とし穴 1: Cache Invalidation
FROM node:20
WORKDIR /app
COPY . . # 1 行変わると全体無効化
RUN npm install
解決: 依存ファイルを先に。
落とし穴 2: 巨大なビルドコンテキスト
$ docker build .
Sending build context: 500 MB # おかしい
原因: .dockerignore なしで node_modules、.git を含んでいる。
解決: .dockerignore を追加。
落とし穴 3: シークレットの漏洩
ARG AWS_SECRET_KEY
ENV AWS_SECRET_KEY=$AWS_SECRET_KEY
# イメージに永遠に残る
解決: --mount=type=secret。
落とし穴 4: 間違った Stage の選択
FROM node:20 AS builder
# ... build stuff ...
FROM node:20-alpine
COPY /app . # alpine に glibc バイナリ
# 実行できない!
解決: base image のアーキテクチャを揃える。
落とし穴 5: プラットフォーム不一致
Apple Silicon でビルド:
docker build -t myapp .
# arm64 でビルドされる
docker push myregistry.com/myapp
# サーバーで実行時に "exec format error"
解決: --platform linux/amd64 を明示。
落とし穴 6: Root User
FROM node:20
# デフォルトで root として実行
CMD ["node", "server.js"]
問題: セキュリティリスク。
解決:
RUN useradd -u 1000 appuser
USER appuser
落とし穴 7: 不要なファイル
COPY . . # すべてのファイル
解決: 必要なファイルのみコピー。
COPY src ./src
COPY package.json tsconfig.json ./
12. デバッグ
Build ログの詳細化
docker buildx build --progress=plain .
# 全ログを出力
中間コンテナの検査
Legacy builder では:
docker commit <intermediate_container_id> debug-image
docker run -it debug-image sh
BuildKit には中間コンテナがない。代わりに:
RUN ...
# 失敗直前の状態をイメージに
または --target で特定 stage までのみ:
docker build --target builder -t myapp:debug .
docker run -it myapp:debug sh
イメージ分析
dive: レイヤーごとのファイルシステムを探索。
dive myapp:latest
- 各レイヤーの内容。
- 無駄な空間。
- サイズ統計。
docker history:
docker history myapp:latest
各レイヤーのコマンドとサイズ。
skopeo: イメージの inspect。
skopeo inspect docker://nginx:latest
ベンチマーク
time docker build -t myapp .
Hyperfine などのツールで繰り返し測定。
クイズで復習
Q1. BuildKit が Legacy builder より速い 3 つの理由は?
A.
1. 並列実行 (Parallelism)
Legacy builder は Dockerfile を 順次 実行する:
FROM → RUN 1 → RUN 2 → RUN 3 → ...
各ステップは前を待つ。独立した作業も順次。
BuildKit は DAG ベース:
FROM ─┬─ RUN 1 (独立)
└─ RUN 2 (独立)
↓
RUN 3 (1, 2 に依存)
独立ステップは 同時実行。Multi-stage build で顕著:
FROM alpine AS a
RUN sleep 10
FROM alpine AS b
RUN sleep 10
FROM alpine
COPY /x /
COPY /y /
- Legacy: 20 秒 (10 + 10)。
- BuildKit: 10 秒 (並列)。
実践では、複数の stage を持つ複雑なビルドで 2 〜 5 倍の高速化。
2. きめ細かな Cache Invalidation
Legacy builder のキャッシュ:
Step 1/5 : COPY . . ← 1 行変わると
Step 2/5 : RUN npm install ← ここも再実行
Step 3/5 : RUN npm build ← ここも
すべて再実行。
BuildKit:
- Cache mount により npm キャッシュを 永続維持。
- ファイル個別 の変更追跡。
- ファイル単位のキャッシュハッシュ。
# BuildKit with cache mount
RUN \
npm ci
結果:
- 初回ビルド: 60 秒。
- 次のビルド: 5 秒 (npm がキャッシュから)。
3. 不要な作業のスキップ
Multi-stage build で BuildKit は 実際に必要な stage のみ ビルド:
FROM node:20 AS deps
RUN npm install
FROM node:20 AS build
COPY /app/node_modules .
RUN npm run build
FROM node:20 AS test
COPY /app/node_modules .
RUN npm test ← 基本ビルドでは不要
FROM node:20-slim
COPY /app/dist ./dist
CMD ["node", "dist/server.js"]
Legacy: test stage もビルド。
BuildKit: 最終イメージが build のみ参照 → test は スキップ。
--target test で明示して test のみ実行することも可能。
パフォーマンス比較の例:
10 stage のある複雑な Dockerfile:
| シナリオ | Legacy | BuildKit |
|---|---|---|
| Cold build | 300s | 180s |
| Warm build (ソース変更) | 300s | 20s |
| Warm build (依存変更) | 300s | 60s |
| 特定 stage のみ | 不可 | 10s |
その他の追加利点:
4. Remote Cache: Registry をキャッシュに:
docker buildx build \
--cache-to type=registry,ref=myapp:cache \
--cache-from type=registry,ref=myapp:cache
CI マシン間でキャッシュ共有。
5. Secret Mount: ビルド中のみシークレットを使い、イメージに残さない。
6. Multi-platform: 一度に複数アーキテクチャ。
教訓:
BuildKit は 単なるアップグレードではなく根本的な再設計。Dockerfile を「コマンドのリスト」ではなく "DAG" として見る視点の変化が、これらすべての最適化を可能にした。
今日 (2025 年) Docker Desktop は BuildKit がデフォルトであり、docker build コマンドも BuildKit を使う。Legacy builder は 過去の話。
実践的な推奨:
- すべての CI/CD: BuildKit 必須。
- ローカル開発: デフォルトで有効 (最新の Docker)。
- キャッシュ管理: Registry cache + gha cache。
- Dockerfile 最適化: BuildKit 機能の活用 (
# syntax=docker/dockerfile:1.6)。
旧式の Dockerfile を BuildKit スタイルに移行する だけで 3 〜 10 倍速いビルド が得られる。コード変更なしで。
Q2. Multi-stage build はどうやってイメージサイズを 100 倍削減するのか?
A.
基本アイデア: 「ビルドツールとランタイムを分離する」。
問題シナリオ: Go アプリのビルド
Go アプリはコンパイルが必要。Docker でビルドするには Go コンパイラを含むイメージが必要:
Multi-stage なし:
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
結果: 〜 1.1 GB イメージ。
なぜ大きいか:
- Go コンパイラ + 標準ライブラリ: 〜 600 MB。
- ビルドキャッシュ (
/go/pkg): 〜 200 MB。 - ソースコード + 依存: 〜 100 MB。
- OS (Debian base): 〜 200 MB。
- 実行に必要なのはバイナリ 1 つだけ (〜 10 MB)。
99% が無駄。
Multi-stage による解決:
# Stage 1: ビルド
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o myapp
# Stage 2: ランタイム
FROM alpine:3.19
COPY /app/myapp /usr/local/bin/
CMD ["myapp"]
結果: 〜 17 MB イメージ。
- Alpine base: 5 MB。
- Go バイナリ: 10 MB。
- CA certificates、tzdata など: 2 MB。
65 倍削減。場合によっては 100 倍 まで可能。
どう動作するか:
Multi-stage build の核心:
- 各 stage は独立: 異なる base image を使用可能。
COPY --from=stage: 前の stage から 選択的にコピー。- 最終 stage が結果イメージ: 以前の stage は イメージに含まれない。
内部的に:
BuildKit は Dockerfile を DAG に変換:
builder (FROM golang:1.21)
│
│ RUN, COPY, ...
│
└→ /app/myapp (artifact)
│
│ COPY --from=builder
↓
runtime (FROM alpine:3.19)
│
└→ 最終イメージ (デプロイ)
builder stage は intermediate。最終イメージに 含まれない。ビルド後 docker images には表示されない。
さらに極端な例: scratch
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
FROM scratch
COPY /app/myapp /
CMD ["/myapp"]
結果: 〜 10 MB (バイナリのみ)。
scratch: 完全に 空 のイメージ。0 バイト。- 必要なのはバイナリだけ。
- Shell なし、ライブラリなし。
制約:
- 静的リンク必須 (
CGO_ENABLED=0)。 - Dynamic 依存がないこと。
- デバッグが困難。
実例: Rust
# Stage 1: Cargo 依存キャッシュ
FROM rust:1.75 AS chef
WORKDIR /app
RUN cargo install cargo-chef
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release
# Stage 2: 最小ランタイム
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY /app/target/release/myapp /usr/local/bin/
CMD ["myapp"]
結果: 80 MB vs 1.5 GB (full Rust image)。
Node.js の複雑な例:
# Stage 1: dev dependencies + ビルド
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci # すべての依存をインストール (dev 含む)
COPY . .
RUN npm run build # TypeScript コンパイルなど
# Stage 2: production dependencies のみ
FROM node:20 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 3: 最小ランタイム
FROM node:20-slim
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY /app/dist ./dist
COPY package*.json ./
CMD ["node", "dist/server.js"]
結果:
node:20: 1.1 GBnode:20+ 全依存 + ビルド: 1.5 GB- この構成: 350 MB
4 倍削減。さらに alpine で最適化すれば 150 MB まで。
利点のまとめ:
1. Pull 速度:
- 1 GB イメージの pull: 30 〜 60 秒。
- 100 MB イメージの pull: 3 〜 5 秒。
- CI/CD 速度、デプロイ速度に大きな影響。
2. ネットワークコスト:
- AWS egress: 0.09 USD/GB。
- 1000 pull/day x 1 GB = 90 USD/day。
- 100 MB イメージ: 9 USD/day。
- 1/10 のコスト。
3. ストレージ:
- Registry 保存。
- Node のイメージキャッシュ。
- 小さいほど節約。
4. セキュリティ:
- 攻撃面の削減。
- 不要なバイナリ = 潜在的 CVE。
- Scratch/distroless = 最小の攻撃面。
5. 起動時間:
- イメージ pull + 起動。
- 小さいイメージ = 速い起動。
- サーバーレス、スケールアウトで重要。
落とし穴と解決:
落とし穴 1: ライブラリ欠落
静的リンクされていないバイナリ:
./myapp: error while loading shared libraries: libcurl.so.4
解決:
- 静的リンク (
-static、CGO_ENABLED=0)。 - または必要なライブラリをコピー。
落とし穴 2: CA Certificates
HTTPS リクエスト時:
x509: certificate signed by unknown authority
解決:
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
または:
FROM scratch
COPY /etc/ssl/certs /etc/ssl/certs
COPY /app/myapp /
落とし穴 3: Timezone
FROM scratch
COPY /usr/share/zoneinfo /usr/share/zoneinfo
ENV TZ=UTC
落とし穴 4: DNS
一部の言語は /etc/nsswitch.conf が必要:
COPY /etc/nsswitch.conf /etc/nsswitch.conf
落とし穴 5: Shell なし
Scratch には shell がない。exec 形式が必須:
CMD ["/myapp"] # exec form (OK)
# 以下は動かない
# CMD /myapp # shell form (NO)
実践的推奨:
| 言語 | 推奨イメージ | サイズ |
|---|---|---|
| Go | scratch | 〜 10 MB |
| Rust | debian-slim | 〜 80 MB |
| Java | eclipse-temurin-jre | 〜 250 MB |
| Node.js | node:alpine | 〜 150 MB |
| Python | python:slim | 〜 120 MB |
| C/C++ | alpine | 〜 5 MB |
教訓:
Multi-stage build は Docker 2.0 の最大の革新の 1 つ (2017 年導入)。単一の Dockerfile に 完全なビルドパイプライン を表現しつつ、最終イメージは最小 に保つ。
これは Docker の 根本的な設計欠陥 を回避する巧妙な方法。以前は「ビルドイメージ」と「ランタイムイメージ」を別々に管理する必要があった。複雑だった。
Multi-stage で:
- 1 つの Dockerfile。
- CI/CD の単純化。
- 最適なイメージサイズ。
- Reproducible。
今日 production Dockerfile の 99% が multi-stage。使わない理由が必要なほど。
あなたの Dockerfile が single-stage なら、multi-stage への移行 が最も簡単な最適化。ほぼ常にイメージが小さくなり、ほぼ常により安全になる。
おわりに: ビルドの進化
要点整理
- OCI spec: コンテナの標準。すべてのツールが遵守。
- Layers + UnionFS: 共有と効率。
- BuildKit: DAG ベース、並列、強力なキャッシュ。
- Multi-stage build: 小さなイメージの鍵。
- Cache mount: ビルド時間の画期的な短縮。
- Secret mount: 安全なシークレット管理。
- Distroless/Scratch: 最小の攻撃面。
- Reproducible builds: 監査と信頼。
実践チェックリスト
Production Dockerfile:
- Multi-stage build。
- Slim/alpine/distroless base。
-
.dockerignoreの設定。 - Dependency ファイルを先に COPY。
- Cache mount (
npm、pip、cargo)。 - Non-root user。
- Secret を
--mount=type=secret経由で。 - Digest pinning (
@sha256:...)。 - HEALTHCHECK の定義。
- Label メタデータ。
- Multi-platform ビルド。
- Image scanning。
- SBOM 生成。
最後の教訓
Docker は コンテナを普及させた。BuildKit は ビルドを再発明した。2 つの革新が出会い、モダン CI/CD の基盤となった。
今日、以下のことが当然になっている:
- 分単位のビルド → 秒単位。
- GB イメージ → MB イメージ。
- 不明瞭なビルド → Reproducible。
- シークレット漏洩 → Secret mount。
しかしこれらすべては Dockerfile を正しく書いたときのみ 達成される。誤った Dockerfile は依然として遅く、大きく、安全でない。
この記事の知識は すべてのエンジニアが知るべきこと。バックエンド、フロントエンド、DevOps、SRE すべて。あなたが作るイメージは 本番で何千回 pull され、何万回実行される。小さく、速く、安全なイメージが システム全体の効率 を決定する。
次の Dockerfile を書くとき、この記事の原則を思い出そう:
- Multi-stage。
- Cache mount。
- Minimal base。
- Order matters。
- Don't leak secrets。
- Think reproducibility。
この習慣が 5 倍速い CI、10 倍小さいイメージ、安全なシステム を作る。