Docker Multi-Stage Buildでコンテナイメージのサイズを削減する
(labs.iximiuz.com)- Dockerコンテナイメージをビルドする際、DockerfileがMulti-Stage構成でない場合は不要なファイルが含まれる可能性が高い
- これはイメージサイズの増加とセキュリティ脆弱性の増加につながる
- コンテナイメージで発生しうる「不要なファイル」の主な原因を分析し、Multi-Stage Buildでこれを解決する方法を説明する
イメージサイズが大きくなる原因
- アプリケーションはビルド時と実行時の依存関係を持つ。
- ビルド時の依存関係は実行時より多く、セキュリティ脆弱性(CVE)も多い。
- 同じイメージをビルドと実行の両方に使うと、不要なビルド時依存関係(コンパイラ、リンターなど)が含まれる。
- ビルド用イメージとランタイムイメージは分離すべきだが、見落とされることが多い。
誤ったDockerfile構成の例
Goアプリケーション向けの誤った例
FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o binary
CMD ["/app/binary"]
golang:1.23イメージはコンパイル用だが、これをそのまま本番環境で使うと、Goコンパイラ全体と依存関係まで含まれてしまう。- イメージサイズ: 800MB以上、800件以上のセキュリティ脆弱性が存在。
Node.jsアプリケーションの誤った例
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "/app/.output/index.mjs"]
node_modulesフォルダに、ランタイムでは不要な開発依存関係まで含まれてしまう。npm ci --omit=devに変更しても解決できず、ビルド工程で必要な開発依存関係まで削除される可能性がある。
Multi-Stage Build以前のLeanイメージ作成方法
Builderパターン
Dockerfile.buildでアプリケーションをビルドする:
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
- ビルド済みアーティファクトをホストへコピーする:
docker cp $(docker create build:v1):/app/.output .
Dockerfile.runでランタイムイメージを生成する:
FROM node:lts-slim
WORKDIR /app
COPY .output .
CMD ["node", "/app/.output/index.mjs"]
• 問題点: 複数のDockerfile作成、ビルド順序の管理が必要、追加スクリプトも必要。
Multi-Stage Buildの理解
- Multi-Stage BuildはDocker内部にBuilderパターンを実装した機能である。
- 複数の
FROM命令を使って、1つのDockerfile内でビルドステージとランタイムステージを定義できる。 COPY --from=<stage>命令を使って、前のステージでビルドしたファイルを取り込む。
- 複数の
Multi-Stage Dockerfileの例(Node.js)
# Build stage
FROM node:lts-slim AS build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Runtime stage
FROM node:lts-slim AS runtime
WORKDIR /app
COPY --from=build /app/.output .
ENV NODE_ENV=production
CMD ["node", "/app/.output/index.mjs"]
COPY --from=buildでビルド済みアーティファクトを直接コピーすることで、ホストを経由せずにファイルを移動できる。
Multi-Stage Buildの実践例
Reactアプリケーション
# Build stage
FROM node:lts-slim AS build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Runtime stage
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
ENTRYPOINT ["nginx", "-g", "daemon off;"]
- Reactアプリケーションはビルド後に静的ファイルとなり、Nginxで配信できる。
Goアプリケーション
# Build stage
FROM golang:1.23 AS build
WORKDIR /app
COPY . .
RUN go build -o binary
# Runtime stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app/binary /app/binary
ENTRYPOINT ["/app/binary"]
distrolessイメージを使い、最小化されたランタイム環境を提供する。
Javaアプリケーション
# Build stage
FROM eclipse-temurin:21-jdk-jammy AS build
WORKDIR /build
COPY . .
RUN ./mvnw package -DskipTests
# Runtime stage
FROM eclipse-temurin:21-jre-jammy
COPY --from=build /build/target/app.jar /app.jar
CMD ["java", "-jar", "/app.jar"]
- ビルドにはJDKを使い、ランタイムではより軽量なJREを使う。
結論
- Multi-Stage Buildはビルド環境とランタイム環境を分離し、不要な開発依存関係によるイメージサイズ増加を防ぐ
- これによりイメージサイズを削減し、セキュリティを強化し、ビルドプロセスを簡素化できる
- Multi-Stage Buildは効率的なコンテナイメージを作るための標準的な方法であり、高度な機能(例: 分岐条件、ビルド中のユニットテスト)もサポートする
6件のコメント
Java の場合、
jlinkはバージョン 9 から導入されていますが、依存モジュールをjdepsで見つけて明示しなければならないなど、使い勝手がよくありません。人々がああした方法を知らなかったり JRE を探していたりするのを見ると、Java ツールの周知が足りないように思えますし、コマンド一つで JRE が生成されるように改善が必要そうです。ああいうふうに使ってはいるんですが、ビルド時間が長くかかるのは欠点な気がします
ビルド時間に差はないはずです。差があるなら設定が間違っているということです!
ああ、そうなんですね!
戦略によっては1つのステージ全体をキャッシュすることもできるので、むしろビルド時間が短縮されることもあるんですよね!
Dockerについてもう少し学ぶ必要がありそうですね!