20
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Java/Mavenビルドが1秒に!Docker Multi-stage Build+BuildKitで98%短縮した話

Posted at

はじめに

GMOコネクトの永田です。

開発中、コードを少し変更するたびに「docker compose build」を実行して数十秒待つ...この繰り返しにストレスを感じたことはありませんか?

今回、JavaアプリケーションのDocker開発環境において、ビルド時間を約39秒から1秒へと98%短縮することに成功しました。この記事では、その最適化プロセスを段階的に解説します。

手元の開発環境で快適なビルド体験を実現したい方の参考になれば幸いです!

まとめ

  • Multi-stage Buildの3ステージ化により、レイヤーキャッシュの効率が38%改善
  • BuildKitキャッシュマウントにより、依存関係のダウンロード時間をほぼゼロに
  • 結果として、ビルド時間を約39秒から1秒へと98%短縮を達成

最適化の結果サマリー

段階 ビルド時間 改善率 主な施策
初期状態 約39秒 - 2ステージビルド
第1段階 約24秒 38%改善 3ステージ化でキャッシュ効率化
第2段階 約1秒 96%改善 BuildKitキャッシュマウント

初回ビルドは依存関係のダウンロードで約68秒かかりますが、2回目以降のビルドは約1秒で完了します。

検証環境

  • MacBook Pro (macOS)
  • Docker Desktop: 4.40.2
  • Docker Compose: v2.40.2
  • アプリケーション構成:
    • ビルド: Maven 3.9 + Eclipse Temurin JDK 21
    • ランタイム: Tomcat 9.0.111 + Amazon Corretto 8
    • データベース: MariaDB 10.11

初期状態: 2ステージのMulti-stage Build

まず、基本的な2ステージビルドから始めました。

Dockerfile(初期バージョン)

# =============================================================================
# Stage 1: ビルドステージ(Maven + JDK 21)
# =============================================================================
FROM maven:3.9-eclipse-temurin-21 AS builder

WORKDIR /build
COPY pom.xml .
COPY app ./app

# Mavenビルド実行
RUN mvn clean package -DskipTests
RUN ls -lh /build/target/app.war

# =============================================================================
# Stage 2: ランタイムステージ(Tomcat + Corretto 8)
# =============================================================================
FROM tomcat:9.0.111-jdk8-corretto-al2

# 必要なパッケージのインストール
RUN yum update -y && \
    yum install -y curl unzip && \
    yum clean all

# Tomcatの初期設定
RUN rm -rf /usr/local/tomcat/webapps/*
RUN mkdir -p /var/log/app && \
    chmod 755 /var/log/app

# ビルドステージからWARファイルをコピー
COPY --from=builder /build/target/app.war /usr/local/tomcat/webapps/app.war

# 起動スクリプトのコピーと実行権限付与
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

EXPOSE 8080
ENTRYPOINT ["docker-entrypoint.sh"]

問題点

このDockerfileには以下の課題がありました:

  1. yum installが毎回実行される: ランタイムステージでパッケージインストールが走り、キャッシュが効きにくい
  2. ソースコード変更のたびに全体が再ビルドされる: レイヤーキャッシュが最大限活用されていない
  3. Maven依存関係が毎回ダウンロードされる: ~/.m2がキャッシュされず、依存関係の再取得で時間がかかる

実測:約39秒

第1段階の最適化: 3ステージ化でキャッシュ効率UP

まず、レイヤーキャッシュを最大限活用するために、Dockerfileを3ステージ構成に再設計しました。

最適化のポイント

  • Stage 1 (runtime-base): 時間がかかるが開発中に変更されない処理(yum install等)を独立させる
  • Stage 2 (builder): Mavenビルド
  • Stage 3 (final): runtime-baseをベースに、ビルド成果物を配置

Dockerfile(3ステージ版)

# =============================================================================
# Stage 1: ランタイムベースの準備
# =============================================================================
FROM tomcat:9.0.111-jdk8-corretto-al2 AS runtime-base

# 必要なパッケージのインストール(この処理は開発中ほぼ変更されない)
RUN yum update -y && \
    yum install -y curl unzip && \
    yum clean all

# Tomcatの初期設定
RUN rm -rf /usr/local/tomcat/webapps/*
RUN mkdir -p /var/log/app && \
    chmod 755 /var/log/app

# =============================================================================
# Stage 2: ビルドステージ(Maven + JDK 21)
# =============================================================================
FROM maven:3.9-eclipse-temurin-21 AS builder

WORKDIR /build

# まずpom.xmlのみをコピー(依存関係の変更は頻繁ではない)
COPY pom.xml .
COPY app ./app

# Mavenビルド実行
RUN mvn clean package -DskipTests
RUN ls -lh /build/target/app.war

# =============================================================================
# Stage 3: 最終イメージ
# =============================================================================
FROM runtime-base

# ビルドステージからWARファイルをコピー
COPY --from=builder /build/target/app.war /usr/local/tomcat/webapps/app.war

# 起動スクリプトのコピーと実行権限付与
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

EXPOSE 8080
ENTRYPOINT ["docker-entrypoint.sh"]

効果

Stage 1のruntime-baseが一度ビルドされると、yum installの処理(約5秒)がキャッシュされるため、ソースコード変更時でも高速にビルドできるようになりました。

実測:約24秒(38%改善)

第2段階の最適化: BuildKitキャッシュマウントで劇的高速化

3ステージ化で改善しましたが、まだMaven依存関係のダウンロードに時間がかかっていました。そこで、BuildKitのキャッシュマウント機能を活用します。

BuildKitとは?

Docker 18.09以降に導入されたビルド機能で、以下の機能があります:

  • 並列ビルドの最適化
  • キャッシュマウント(--mount=type=cache)による永続的なキャッシュ
  • より効率的なレイヤーキャッシュ

Docker Compose v2ではデフォルトで有効になっています。

Dockerfile(最終版 - BuildKitキャッシュマウント対応)

# =============================================================================
# Stage 1: ランタイムベースの準備
# =============================================================================
FROM tomcat:9.0.111-jdk8-corretto-al2 AS runtime-base

# 必要なパッケージのインストール
RUN yum update -y && \
    yum install -y curl unzip && \
    yum clean all

# Tomcatの初期設定
RUN rm -rf /usr/local/tomcat/webapps/*
RUN mkdir -p /var/log/app && \
    chmod 755 /var/log/app

# =============================================================================
# Stage 2: ビルドステージ(Maven + JDK 21)
# =============================================================================
FROM maven:3.9-eclipse-temurin-21 AS builder

WORKDIR /build

# pom.xmlを先にコピーして依存関係をダウンロード
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B

# ソースコードをコピー
COPY app ./app
RUN --mount=type=cache,target=/root/.m2 \
    mvn clean package -DskipTests

RUN ls -lh /build/target/app.war

# =============================================================================
# Stage 3: 最終イメージ
# =============================================================================
FROM runtime-base

# ビルドステージからWARファイルをコピー
COPY --from=builder /build/target/app.war /usr/local/tomcat/webapps/app.war

# 起動スクリプトのコピーと実行権限付与
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

EXPOSE 8080
ENTRYPOINT ["docker-entrypoint.sh"]

最適化のキーポイント

1. キャッシュマウントの活用

RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B

--mount=type=cache,target=/root/.m2 により、Maven Local Repository(~/.m2)がビルド間で永続化されます。これにより、依存関係のダウンロードが初回のみとなります。

2. pom.xmlとソースコードの分離

# 1. pom.xmlのみコピー → 依存関係ダウンロード
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B

# 2. その後にソースコードをコピー → ビルド
COPY app ./app
RUN --mount=type=cache,target=/root/.m2 \
    mvn clean package -DskipTests

この分離により:

  • pom.xml変更時: 依存関係の再ダウンロードのみ
  • ソースコード変更時: ビルドのみ(依存関係はキャッシュ利用)

3. dependency:go-offline の活用

mvn dependency:go-offline -B

すべての依存関係を事前にダウンロードし、キャッシュに保存します。

docker-compose.yml での設定

BuildKitは Docker Compose v2 でデフォルト有効ですが、明示的に有効化する場合は環境変数を設定します:

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: app
    # ... その他の設定

ビルドコマンド実行時:

# BuildKitを明示的に有効化(v2ではデフォルトで有効)
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

# ビルド実行
docker compose build app

効果

初回ビルド: 約68秒(依存関係の完全ダウンロード)
2回目以降: 約1秒(96%改善!)

$ time docker compose build app
[+] Building 0.9s (22/22) FINISHED
 => CACHED [runtime-base 2/4] RUN yum update -y && ...
 => CACHED [builder 4/7] RUN --mount=type=cache,target=/root/.m2 ...
 => CACHED [builder 6/7] RUN --mount=type=cache,target=/root/.m2 ...
...
docker compose build app  0.12s user 0.08s system 19% cpu 1.053 total

すべてのステージがCACHEDとなり、レイヤーキャッシュが完全に効いています!

動作確認

最適化後の環境で、実際に動作確認を行います。

# コンテナ起動
$ docker compose up -d
[+] Running 2/2
 ✔ Container app-db   Healthy
 ✔ Container app      Started

# アプリケーションログ確認
$ docker compose logs app | tail -5
app  | Server startup in [727] milliseconds

# APIテスト
$ curl -s "http://localhost:8080/api/health" | jq '.'
{
  "status": "OK",
  "uptime": "00:01:23"
}

完璧に動作しています!

最適化のベストプラクティスまとめ

今回の最適化を通じて学んだベストプラクティスをまとめます。

1. Multi-stage Buildの構成

開発時に変更されない処理(runtime-base)
    ↓
ビルド処理(builder)
    ↓
最終イメージ(final)

時間がかかるが変更頻度の低い処理を独立させることで、キャッシュ効率を最大化します。

2. BuildKitキャッシュマウントの活用

# Maven Local Repository をキャッシュ
RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B

# npm の場合
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# pip の場合
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

各言語のパッケージマネージャーのキャッシュディレクトリをマウントすることで、依存関係のダウンロード時間を劇的に短縮できます。

3. レイヤーの順序最適化

# ✅ Good: 変更頻度の低いものから順に
COPY pom.xml .              # 依存関係定義(低頻度)
RUN mvn dependency:go-offline
COPY app ./app              # ソースコード(高頻度)
RUN mvn clean package

# ❌ Bad: まとめてコピー
COPY . .                    # すべてコピーしてしまう
RUN mvn clean package       # ソース変更で依存関係も再取得

変更頻度の低いファイルを先にCOPYすることで、キャッシュヒット率が向上します。

4. .dockerignore の活用

# .dockerignore
target/
*.log
.git/
.DS_Store
node_modules/

不要なファイルをビルドコンテキストから除外し、転送時間を削減します。

トラブルシューティング

BuildKitが有効にならない場合

Docker Compose v1を使用している場合は、以下を設定:

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

または、~/.docker/config.json に追加:

{
  "features": {
    "buildkit": true
  }
}

キャッシュをクリアしたい場合

# ビルドキャッシュをクリア
docker builder prune

# BuildKitのキャッシュマウントもクリア
docker builder prune --all

キャッシュマウントが効かない場合

  • Docker Desktopのバージョンを確認(18.09以降が必要)
  • Docker Composeのバージョンを確認(v2推奨)
  • BuildKit機能が有効になっているか確認
docker version
# BuildKit: が表示されればOK

パフォーマンス比較表

項目 初期状態 3ステージ化 BuildKitキャッシュ
ビルド時間(初回) 39秒 39秒 68秒※
ビルド時間(2回目) 39秒 24秒 1秒
ソース変更後 39秒 24秒 1秒
pom.xml変更後 39秒 24秒 約10秒
完全クリーン後 39秒 39秒 68秒※

※ 初回は依存関係の完全ダウンロードが必要だが、以降は高速

(再掲)まとめ

  • Multi-stage Buildの3ステージ化により、レイヤーキャッシュの効率が38%改善
  • BuildKitキャッシュマウントにより、依存関係のダウンロード時間をほぼゼロに
  • 結果として、ビルド時間を約39秒から1秒へと98%短縮を達成

今回紹介した手法は、Maven以外のビルドツール(Gradle、npm、pipなど)でも応用可能です。ぜひ、皆さんのプロジェクトでも試してみてください!

最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ: https://gmo-connect.jp/contactus/

参考リンク

20
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?