🐳

Spring Boot コンテナビルド戦略:Dockerfile と Buildpacks、どちらを選ぶべきか?

に公開

はじめに

Spring Boot アプリケーションを Docker コンテナ化する際、多くの開発者が直面する選択肢があります。それは、Dockerfile を記述してビルドプロセスを細かく制御する道か、それとも Cloud Native Buildpacks によるビルド(./gradlew bootBuildImage コマンド)による手軽な自動化の道か、という選択です。

Dockerfile を使えば、マルチステージビルドを駆使して、本番環境に最適化された極限まで軽量なイメージを追求できます。また、Docker に慣れている開発者にとっては慣れているため使いやすいでしょう。一方、./gradlew bootBuildImage コマンド一つで最適なコンテナイメージの生成もできます。Spring Boot アプリケーション開発者にとっては、Docker についての知識があまりなくても済むので、非常に魅力的です。

では、どちらの戦略を選ぶべきなのでしょうか?

答えは一つではありません。それは、イメージサイズと完全なコントロールを求めるのか、それとも手軽さと自動化を重視するのか、といったプロジェクトの要件やフェーズによって異なります。

本記事では、この2つの主要なビルド戦略を、ハンズオン形式で比較します。

コンテナに最適化された BellSoft Liberica JDK を活用し、それぞれの方法でイメージサイズやビルドプロセスがどう変わるのかを具体的に示しながら、あなたのプロジェクトに最適な選択肢を見つける手助けをします。

なお、Buildpacks で手軽にコンテナイメージが作成できるとはいえ、Dockerfile を使ってコンテナイメージを作成するのが基本なので、最初に Dockerfile を使う方法について説明します。その後、Buildpacks を使う方法について説明します。

本番環境を最適化する Dockerfile の設計

最初に本番環境向けの Dockerfile の設計をします。アプリケーションをビルドするための「ビルド環境」と、ビルドされたアプリケーションを実行するための「本番環境」を1つの Dockerfile で実現する「マルチステージビルド」を利用します。

なぜ「マルチステージビルド」が必要なのか?

ビルド環境では、ソースコードをコンパイルし、実行可能な JAR ファイルを生成するために、JDK(Java Development Kit)やビルドツール(Gradle/Maven)が必要です。これらは比較的多機能でサイズも大きくなります。

一方、本番環境では、生成された JAR ファイルを実行するためには、JRE(Java Runtime Environment)さえあれば十分です。JDK やビルドツールは不要です。

マルチステージビルドは、この2つの環境を Dockerfile 内で明確に分離し、最終的なコンテナイメージには「本番環境」に必要なものだけを含めるための仕組みです。これにより、本番イメージの軽量化とセキュリティ向上を実現します。

なぜ「BellSoft Liberica JDK」を選ぶのか?

数ある OpenJDK ディストリビューションの中から、本記事では BellSoft 社が提供する Liberica JDK を選択しました。その理由は、Liberica JDK が特に Spring Bootコンテナ環境 との親和性を重視して設計されているためです。

Spring Boot に推奨される JDK

Liberica JDK は、Spring Framework の開発チームに公式に推奨されている JDK の一つです。https://spring.io/quickstart のサイトの説明にある必要なものとして JDK があり、そこに We recommend BellSoft Liberica JDK version 17 or 21. とあります。

また、spring init コマンドで作成した Spring Boot プロジェクトは、Dockerfile を用意しなくても、アプリケーションを実行するコンテナイメージを作成するタスクが使えるようになっています。そのタスクを実行したときにデフォルトで使用されるのが Liberica JDK となります。

これらの理由から、Spring Boot アプリケーションを開発する際には、Liberica JDK での動作確認が前提となっていることが多いと認識しておくのが良いです。

コンテナに最適化されたランタイム

Liberica JDK の最大の特徴は、コンテナ環境に特化した軽量なランタイムイメージを提供している点です。一般的に利用されるベース Docker イメージとしては、次の2つがあります。

イメージ名 目的/用途
bellsoft/liberica-openjdk-debian 開発・ビルド環境
bellsoft/liberica-runtime-container 本番実行環境

bellsoft/liberica-openjdk-debian は、JDK (Java Development Kit) の全機能を含むイメージです。ソースコードのコンパイルやデバッグに必要なツールが含まれており、マルチステージビルドの「ビルド環境」に適しています。

bellsoft/liberica-runtime-container は、JRE (Java Runtime Environment) に相当する、アプリケーションの実行に特化した最小限のコンポーネントのみを含むイメージです。JDK の軽量バージョンである JDK Lite のイメージもあります。不要なツールが削ぎ落とされているため、イメージサイズが非常に小さく、セキュリティリスクも低減されます。本番の「実行環境」に最適です。

TCK 準拠と長期サポート (LTS)

もちろん、Liberica JDK は Java SE の TCK (Technology Compatibility Kit) に完全に準拠しており、Java の標準仕様との互換性が保証されています。また、LTS (長期サポート) バージョンに対して定期的なセキュリティパッチやアップデートが提供されるため、本番環境でも安心して利用できます。

本記事では、これらの特徴を持つ Liberica JDK のイメージをマルチステージビルドで使い分けることで、開発の利便性を損なうことなく、本番環境のコンテナを最適化する方法を実証します。

マルチステージビルド Dockerfile の例

それでは、spring-bellsoftプロジェクトのルートに、以下の内容でDockerfileを作成してください。

# Build stage
FROM bellsoft/liberica-openjdk-debian:17 AS build
WORKDIR /app
COPY gradlew build.gradle.kts settings.gradle.kts ./
COPY gradle ./gradle
RUN ./gradlew dependencies
COPY src ./src
RUN ./gradlew build -i
RUN rm /app/build/libs/*-plain.jar

# Final stage
FROM bellsoft/liberica-runtime-container:jdk-17-glibc
WORKDIR /
COPY --from=build /app/build/libs/*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

この Dockerfile のポイントは2つです。

  1. AS buildによるステージの命名
  2. COPY --from=buildによる成果物のコピー

ここでは、最初に FROM 命令時に AS build と名付けることで、このステージを「ビルドステージ」として定義しています。

それから、最終ステージ(Final stage)で、--from=build を指定することで、先ほどのビルドステージからビルド成果物(*.jar ファイル)だけをコピーしています。

これにより、最終的に出来上がるコンテナイメージには、ソースコードやビルドツールは一切含まれず、実行に必要な app.jar と軽量な Java ランタイムのみが含まれることになります。

また、ビルドステージと最終ステージで bellsoft/liberica-openjdk-debian:17bellsoft/liberica-runtime-container:jdk-17-glibc という、同じバージョン 17 のJDK/JREと、同じベース OS(Debian/glibc)のイメージを選択している点も重要です。これにより、OS・ライブラリ・JVM のバージョンを揃え、環境差異による予期せぬ問題を最小限に抑えています。

ハンズオンの準備:Spring Boot プロジェクト作成

まずは、ハンズオンの題材となるシンプルな Spring Boot アプリケーションを作成します。

Spring Boot CLI を使う場合

Spring Boot CLI がインストールされている環境であれば spring コマンドが使えます。以下のコマンドを実行してください。

spring init \
  --type=gradle-project-kotlin \
  --language=java \
  --bootVersion=3.5.7 \
  --name=spring-bellsoft \
  --groupId=internal.dev \
  --artifactId=spring-bellsoft \
  --packageName=internal.dev.spring_bellsoft \
  --dependencies=web \
  --javaVersion=17 \
  spring-bellsoft

これで、spring-bellsoft フォルダが作成され、Spring Boot アプリケーションの開発に必要な環境が用意されます。

Spring Boot CLI 用コンテナを使う場合

Spring Boot CLI 用コンテナについては次の記事を公開しています。

この記事で紹介した dev-springboot-cli:3.5.7 イメージがあるなら、次のように実行します。このイメージを使ったコンテナは、起動時に spring コマンドを実行するので、docker container run ... dev-springboot-cli:3.5.7 の実行は、spring コマンドを実行するのと同じことになります。

docker container run --rm -it \
  -v $(pwd):/workspace \
  -w /workspace \
  dev-springboot-cli:3.5.7 \
  init \
    --type=gradle-project-kotlin \
    --language=java \
    --bootVersion=3.5.7 \
    --name=spring-bellsoft \
    --groupId=internal.dev \
    --artifactId=spring-bellsoft \
    --packageName=internal.dev.spring_bellsoft \
    --dependencies=web \
    --javaVersion=17 \
    spring-bellsoft

Spring Initializr を利用する場合

Spring Initializr のサイトへアクセスして、Gradle プロジェクトを作成することもできます。

指定する内容は次のようになります。() 内は、後で説明する Spring Boot CLI で対応するオプションです。コマンド例では、--description--packaging--configurationFileFormat は省略していましたが、省略すると初期値となります。

  • Project: Gradle - Kotlin (--type=gradle-project-kotlin)
  • Language: Java (--language=java)
  • Spring Boot: 3.5.7 (--bootVersion=3.5.7)

Project Metadata の各項目については次のとおりです。

  • Group: internal.dev (--groupId=internal.dev)
  • Artifact: (--artifactId=spring-bellsoft)
  • Name: spring-bellsoft (--name=spring-bellsoft)
  • Description: Demo project for Spring Boot (--description="Demo project for Spring Boot")
  • Package name: internal.dev.spring_bellsoft (--packageName=internal.dev.spring_bellsoft)
  • Packaging: Jar (--packaging=jar)
  • Configuration: Properties (--configurationFileFormat=properties)
  • Java: 17 (--javaVersion=17)

Dependencies は「ADD DEPENDENCIES」をクリックすると表示される一覧で「Spring Web」をクリックして追加します。

  • Dependencies: Spring Web (--dependencies=web)

指定ができたら「GENERATE」をクリックすると zip 形式のアーカイブファイルがダウンロードできます。これを展開すると、サンプルの Spring プロジェクトのファイルが入手できます。

アプリケーションのカスタマイズ

最初に spring-bellsoft をカレントフォルダとしておきます。

cd spring-bellsoft

次に、動作確認用の簡単な Web コントローラー(@RestController アノテーションをつけた REST コントローラークラス)を作成します。

cat << EOS > ./src/main/java/internal/dev/spring_bellsoft/RootController.java
package internal.dev.spring_bellsoft;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RootController {

    @GetMapping("/")
    public String index() {
        return "Hello Spring";
    }
}
EOS

また、後でコンテナ内で動作する Java のバージョン情報を確認できるように /actuator/info エンドポイント関連の設定を追加します。

cat <<EOF > ./src/main/resources/application.properties
management.endpoints.web.exposure.include=health,info
management.info.java.enabled=true
management.info.os.enabled=true
EOF

なお、/actuator/info エンドポイントが使えるようにするためには、build.gradle.kts ファイルで指定されている依存関係に org.springframework.boot:spring-boot-starter-actuator パッケージを追加する必要があります。

// 略
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-actuator")  // 追加
    implementation("org.springframework.boot:spring-boot-starter-web")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
// 略

sed コマンドで build.gradle.kts を編集するには、次のようにします。

sed -i 's/dependencies {/dependencies {\n\timplementation\("org.springframework.boot:spring-boot-starter-actuator"\)/' \
   build.gradle.kts

参考までにローカルマシンに Java 開発環境がある場合は、次のコマンドで JAR ファイルの生成をするビルドタスクが実行できます。

./gradlew build

これで、後で使う build/libs/spring-bellsoft-0.0.1-SNAPSHOT.jar ファイルが生成されます。ローカルマシンに Java 開発環境がない場合は、後で説明する開発コンテナを使って用意できます。

Docker 対応

Docker イメージに含める Spring Boot アプリケーションのプロジェクトについて準備ができたので、Docker 対応します。ここでは、次のフォルダ構成で、いくつかの Docker 用ファイルを作成します。

spring-bellsoft/
├── Dockerfile ... マルチステージビルド 用 Dockerfile
├── compose.yaml ... ビルド、動作確認用 Docker Compose 設定ファイル
├── docker/
│   ├── spring-bellsoft-simple/
│   │   ├── Dockerfile ... ビルド済み JAR + Liberica JDK 用 Dockerfile
│   │   └── compose.yaml ... spring-bellsoft:simple-0.0.1 用 Docker Compose 設定ファイル
│   └── spring-temurin-simple/
│       ├── Dockerfile ... ビルド済み JAR + Temurin JDK 用 Dockerfile
│       └── compose.yaml ... spring-temurin:simple-0.0.1 用 Docker Compose 設定ファイル
//--------
// これ以降にあるものは spring init で作成したもの
├── README.md
├── build.gradle.kts
├── gradle/
│   └── wrapper/
├── gradlew*
├── gradlew.bat
├── settings.gradle.kts
└── src/
    ├── main/
    └── test/

これ以降、spring-bellsoft フォルダのパスを ${PROJECT_DIR} と表記します。

Dockerfile と compose.yaml の作成

すでに Dockerfile は提示してありますが、再掲載しておきます。次の内容で作成します。

# Build stage
FROM bellsoft/liberica-openjdk-debian:17 AS build
WORKDIR /app
COPY gradlew build.gradle.kts settings.gradle.kts ./
COPY gradle ./gradle
RUN ./gradlew dependencies
COPY src ./src
RUN ./gradlew build -i
RUN rm /app/build/libs/*-plain.jar

# Final stage
FROM bellsoft/liberica-runtime-container:jdk-17-glibc
WORKDIR /
COPY --from=build /app/build/libs/*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

Dockerfile だけでは、Docker イメージ名の情報がわからなくなるので、compose.yaml を用意して宣言します。プロジェクト用の compose.yaml を用意しておくと、Docker イメージの基本的な使い方のサンプルとしても役に立つので、基本的に用意するようにしています。

このファイルは、アプリケーションを動かすための「サービス」の名前や、コンテナの「作り方」「動かし方」を定義しています。

name: spring-bellsoft
services:
  spring-bellsoft:
    build:
      context: ./
      dockerfile: ./Dockerfile
    image: spring-bellsoft:0.0.1
    container_name: spring-bellsoft
    ports:
      - 127.0.0.1:8080:8080

簡単なファイルですが、各設定項目の詳細について解説しておきます。

Docker Compose プロジェクトの名前 (name):

name: spring-bellsoft

この設定は、この Docker Compose ファイルで作成する Docker Compose プロジェクト全体につける名前です。ここでは、spring-bellsoft という名前を設定しています。

サービスの設定 (services):

services:
  spring-bellsoft:

services は、実際に動かしたいコンテナの定義をまとめる場所です。その下の spring-bellsoft: は、このコンテナサービスの名前を定義しています。

イメージのビルド設定 (build):

    build:
      context: ./
      dockerfile: ./Dockerfile

この部分は、「このサービスを動かすための Docker イメージのビルド」について設定しています。

context: には、Docker イメージ作成時のルートとなる作業フォルダのパスを指定します。./は、この compose.yaml ファイルがあるのと同じフォルダを意味します。

dockerfile: には、実際にイメージを作るための手順書である Dockerfile のファイルパスを指定します。./Dockerfile は、この compose.yaml ファイルがあるのと同じフォルダにある Dockerfile を意味します。

イメージ名 (image):

    image: spring-bellsoft:0.0.1

image: には、作成した Docker イメージにつける名前とバージョンを指定します。一度イメージを作成すれば、この名前で再利用できます。

コンテナの識別名 (container_name):

container_name: には、実際に実行するコンテナにつける名前を指定します。この名前でコンテナを識別できるので、管理が簡単になります。

ポートの設定 (ports):

    ports:
      - 127.0.0.1:8080:8080

この設定は、コンテナ外部からコンテナ内のアプリケーションにアクセスできるようにするために最も重要な部分です。

127.0.0.1:8080:8080 の構造は、[アクセス制限IP]:[PC側のポート]:[コンテナ側のポート]となっています。

127.0.0.1 は「自分の PC からしかアクセスを許可しない」という制限をかけています。他のマシンから接続する必要がないのなら、指定しておくのが無難です。

127.0.0.1 に続く :8080 は、[PC側のポート] で、自分の PC(ホストOS)が使うポート番号です。今回の Spring Boot アプリケーションの場合は、自分の PC で動作する Web ブラウザから http://127.0.0.1:8080 でアクセスできるようにするために指定します。

[PC側のポート] に続く :8080 は、[コンテナ側のポート] で、コンテナ内で Spring Boot アプリケーションが接続待機をするために使うポート番号です。

これで、皆さんの PC でこの設定ファイルを使って Docker Compose プロジェクトのサービスを実行すれば、手軽に Spring Boot アプリケーションのコンテナを立ち上げることができます。

Docker コンテナのビルドと実行

Dockerfilecompose.yaml の用意ができたら、これらを使用して、Docker イメージをビルドします。

cd ${PROJECT_DIR}
docker compose build

ここで、Spring Boot アプリケーションのコードに問題があると、JAR のビルドが失敗します。JAR のビルドに失敗する場合は、エラーを確認して適切な対応をします。Java 開発環境があるなら、その環境で手動で JAR のビルドが成功するところまで確認すると良いです。後で紹介する開発コンテナを使ってJAR のビルドの確認することもできます。

ここではJAR のビルドが成功し、Docker イメージのビルドも成功したとして勧めます。コンテナを起動するには次のコマンドを実行します。

docker compose up -d

起動すると、docker compose ls を実行したときに、一覧に spring-bellsoft のものが表示されます。STATUSrunning(1) となっていれば大丈夫です。

$ docker compose ls
NAME                STATUS      CONFIG FILES
spring-bellsoft     running(1)  (略)

なお、Docker コンテナを停止し、削除する場合は次のように docker compose down コマンドを実行します。-p spring-bellsoft をつけないと、カレントフォルダが ${PROJECT_DIR} でない場合にコマンドが失敗します。

docker compose -p spring-bellsoft down

コンテナの動作確認

コンテナが起動したら、動作確認をします。動作確認をする前に、次のようにログを確認します。

docker compose -p spring-bellsoft logs

実行例は次のようになります。

$ docker compose -p spring-bellsoft logs
spring-bellsoft  | 
spring-bellsoft  |   .   ____          _            __ _ _
spring-bellsoft  |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
spring-bellsoft  | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
spring-bellsoft  |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
spring-bellsoft  |   '  |____| .__|_| |_|_| |_\__, | / / / /
spring-bellsoft  |  =========|_|==============|___/=/_/_/_/
spring-bellsoft  | 
spring-bellsoft  |  :: Spring Boot ::                (v3.5.7)
(略)

ログメッセージの最後の方に、Tomcat started on port ... や、Started SpringBellsoftApplication ... といったものがあれば Spring Boot アプリケーションが確実に動作しています。

次の URL へ Web ブラウザや curl コマンドで接続して、動作することを確認します。

curl コマンドを使った場合の実行例は次のようになります。

ルートへのアクセスでは Hello Spring のメッセージが表示されます。

$ curl http://localhost:8080/
Hello Spring

actuator/info へのアクセスでは、Java の情報が表示されます。

curl -s localhost:8080/actuator/info | jq .

結果から使用されている Java の情報が取得できます。

{
  "java": {
    "version": "17.0.17",
    "vendor": {
      "name": "BellSoft"
    },
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "17.0.17+11-LTS"
    },
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "BellSoft",
      "version": "17.0.17+11-LTS"
    }
  },
  "os": {
    "name": "Linux",
    "version": "6.14.0-35-generic",
    "arch": "amd64"
  }
}

ローカルビルド済み JAR 用 Dockerfile

開発作業をしているマシンでは、事前に ./gradlew build が完了していて、JAR ファイルが用意できていることが多いでしょう。そういった場合に対応できるように、Dockerfilecompose.yaml を別に用意しておきます。こちらを使うと、Docker 内でのビルドが不要なため時間短縮になります。

ここでは、Liberica JDK をベースとするものを docker/spring-bellsoft-simple/ に、Temurin JDK をベースとするものを docker/spring-temurin-simple/ に用意しました。

spring-bellsoft-simple 版

docker/spring-bellsoft-simple/Dockerfile は次のようになります。

FROM bellsoft/liberica-runtime-container:jdk-17-glibc
ARG JAR_FILE=build/libs/spring-bellsoft-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

docker/spring-bellsoft-simple/compose.yaml は次のようになります。

name: spring-bellsoft-simple
services:
  spring-bellsoft-simple:
    build:
      context: ../..
      dockerfile: ./docker/spring-bellsoft-simple/Dockerfile
    image: spring-bellsoft:simple-0.0.1
    container_name: spring-bellsoft-simple
    ports:
      - 127.0.0.1:8080:8080

spring-temurin-simple 版

docker/spring-temurin-simple/Dockerfile は次のようになります。

FROM eclipse-temurin:17-jdk-jammy
ARG JAR_FILE=build/libs/spring-bellsoft-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

docker/spring-temurin-simple/compose.yaml は次のようになります。

name: spring-temurin-simple
services:
  spring-temurin-simple:
    build:
      context: ../..
      dockerfile: ./docker/spring-temurin-simple/Dockerfile
    image: spring-temurin:simple-0.0.1
    container_name: spring-temurin-simple
    ports:
      - 127.0.0.1:8080:8080

spring-bellsoft-simple 版と spring-temurin-simple 版のビルド

これらを実際に使うには build/libs/spring-bellsoft-0.0.1-SNAPSHOT.jar ファイルが必要です。前にも説明しましたが、ローカルマシンに Java 開発環境がない場合は、後で説明する開発コンテナを使って用意できます。

それぞれについて、Docker イメージビルド、サービス起動、サービス終了のコマンドは次のようになります。

cd ${PROJECT_DIR}
# spring-bellsoft-simple 用のコマンド
docker compose -f docker/spring-bellsoft-simple/compose.yaml build
docker compose -f docker/spring-bellsoft-simple/compose.yaml up -d
docker compose -p spring-bellsoft-simple down
# spring-temurin-simple 用のコマンド
docker compose -f docker/spring-temurin-simple/compose.yaml build
docker compose -f docker/spring-temurin-simple/compose.yaml up -d
docker compose -p spring-temurin-simple down

イメージサイズの比較

BellSoft Liberica JDK を使った場合と、標準的な Eclipse Temurin JDK を使った場合のイメージサイズを比較します。

確認するには docker image ls コマンドが使えます。--format オプションで "{{.Size}}\t{{.Repository}}:{{.Tag}}" を指定して、サイズとリポジトリ名:タグ名のペアを一覧表示するようにします。

docker image ls --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" \
  | grep spring \
  | sort

実行結果は次のようになりました。

$ docker image ls --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" \
  | grep spring \
  | sort
138MB   spring-bellsoft:0.0.1
138MB   spring-bellsoft:simple-0.0.1
435MB   spring-temurin:simple-0.0.1

この結果から、BellSoft Liberica JDK をベースにしたイメージは、Temurin JDK を利用した場合に比べて大幅に小さいことがわかります。

また、ローカルマシンでビルドした JAR ファイルと Docker イメージ作成時にビルドした JAR ファイルにはサイズに差がないこともわかります。

bootBuildImage タスクで Docker イメージ作成

spring init で作成した Spring Boot プロジェクトの場合は、Dockerfile を使わずに Docker イメージを作成することもできます。今回のように Gradle を使っている場合は、bootBuildImage タスクを実行します。

cd ${PROJECT_DIR}
./gradlew bootBuildImage

これで、spring-bellsoft:0.0.1-SNAPSHOT という Docker イメージが作成されます。

./gradlew bootBuildImage タスクは実行時に Cloud Native Buildpacks(CNB)を使用し、これはデフォルトで Liberica JDK を含む Docker イメージを作成します。

Cloud Native Buildpacks というのは、ソースコードを OCI (Open Container Initiative) 準拠のコンテナイメージに変換するプロセスを自動化する標準およびツールです。公式サイトの URL は次のとおりです。

なお、本記事執筆時点(2025-11-13)では、最新の Docker Engine 29.0 でこのタスクが失敗する事象が発生しました。そのため、本記事では次に紹介する Docker in Docker が利用可能な開発コンテナを用意し、作業を進める方法を提示します。

開発コンテナ用設定ファイルの作成

開発コンテナを使えるようにするために、spring-bellsoft/ フォルダに .devcontainer/devcontainer.json ファイルを用意します。

spring-bellsoft/
├── .devcontainer/
│   └── devcontainer.json
// その他のファイル

devcontainer.json ファイルの内容は次のようにします。

{
  "name": "dvc-spring-bellsoft",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
  "mounts": [
    {
      "source": "${localWorkspaceFolder}",
      "target": "/workspaces/${localWorkspaceFolderBasename}",
      "type": "bind"
    },
    {
      "source": "${localWorkspaceFolderBasename}-dind-data",
      "target": "/var/lib/docker",
      "type": "volume"
    },
    {
      "source": "${localWorkspaceFolderBasename}-home-vscode-gradle",
      "target": "/home/vscode/.gradle",
      "type": "volume"
    }    
  ],
  "features": {
    "ghcr.io/devcontainers/features/java:1": {
      "version": "17.0.17-librca",
      "jdkDistro": "liberica",
      "installGradle": true
    },
    "ghcr.io/devcontainers/features/docker-in-docker:2": {
      "version": "28.5.2",
      "moby": false
    }
  },
  "runArgs": ["--privileged"],
  "customizations": {
    "vscode": {
      "extensions": [
        "vscjava.vscode-java-pack",
        "ms-azuretools.vscode-docker"
      ]
    }
  },
  "forwardPorts": [8080],
  "postCreateCommand": "sudo chown vscode: /home/vscode/.gradle && java -version && gradle --version && docker --version"
}

このファイルについては重要なポイントのみ説明します。

Microsoft が提供する開発コンテナのイメージとして、筆者は Ubuntu ベースのものを頻繁に利用しています。ここでは Ubuntu 24.04 をベースとして mcr.microsoft.com/devcontainers/base:ubuntu-24.04 を指定しました。Ubuntu は Debian ベースなので、実行時に使うものに近いものとなります。

また、フィーチャーとして javadocker-in-docker を指定することで、javacdocker コマンドが使えるようになります。docker-in-docker はコンテナ内に Docker 環境を構築するためのものなので、"runArgs": ["--privileged"] の指定が必要になります。

なお、この開発コンテナでは、"mounts": の指定で、Docker イメージ保管用の /var/lib/docker と、Gradle のデータ保管用の /home/vscode/.gradle を Docker ボリュームで永続化するようにしています。こうすることで開発コンテナを削除しても、次回の開始時に Docker イメージや Gradle のデータを再利用できるようになります。

もし不要な場合は、開発コンテナを削除した後に、docker volume rm コマンドで Docker ボリュームを削除してください。次のコマンドで削除できます。

docker volume rm spring-bellsoft-dind-data
docker volume rm spring-bellsoft-home-vscode-gradle

また、そもそも永続化が不要な場合は "type": "volume" を含む要素を "mounts":[] の配列内から削除してください。

開発コンテナの利用

ファイルを作成したら、VS Code を起動して、開発コンテナを使います。

cd ${PROJECT_DIR}
code .

VS Code の画面で「コンテナで再度開く」という通知が出たらクリックして開発コンテナを起動します。通常は開発コンテナをアタッチした VS Code の画面が表示されます。

しかし、本記事執筆時点(2025-11-13)では、このプロセスが正常に進行しない事象が発生しました。その場合は、新しい VS Code 画面を開き、コンテナビューでコンテナ一覧を表示し、該当する vsc-spring-bellsoft-(環境により変わる uid) コンテナを右クリックして「Visual Studio Code をアタッチする」を選択することで、開発コンテナにアタッチした VS Code の画面を表示できます。

それから、開発コンテナにアタッチした VS Code の画面でターミナルを開いて作業します。

最初にカレントフォルダを、/workspaces/spring-bellsoft とします。

cd /workspaces/spring-bellsoft

それから、プロジェクトが依存するパッケージをダウンロードします。

./gradlew dependencies

次に JAR ファイルを生成します。これで build/libs/spring-bellsoft-0.0.1-SNAPSHOT.jar が用意されます。Docker ホストの spring-bellsoft フォルダをバインドマウントしているので、同じファイルが Docker ホストにもできているはずです。

./gradlew build

次に Docker イメージを作成する bootBuildImage タスクを実行します。

./gradlew bootBuildImage

成功したら Docker イメージを docker image save コマンドで保存します。開発コンテナ内で動いている Docker デーモンと、開発コンテナを動かしている Docker デーモンは別なので、このままだと、開発コンテナ内でしか、このイメージが実行できません。

docker image save spring-bellsoft:0.0.1-SNAPSHOT \
  -o /workspaces/spring-bellsoft/spring-bellsoft_0.0.1-SNAPSHOT.tar

これで、Docker ホスト(開発コンテナを動かしている Docker デーモン)の ${PROJECT_DIR}spring-bellsoft_0.0.1-SNAPSHOT.tar ファイルが作成されます。

これを Docker ホストのターミナルでロードします。

cd ${PROJECT_DIR}
docker image load -i spring-bellsoft_0.0.1-SNAPSHOT.tar

これで、Docker ホストに spring-bellsoft:0.0.1-SNAPSHOT イメージがロードされて、使えるようになります。

ちなみにイメージサイズは 239MB でした。

$ docker image ls --format "{{.Size}}\t{{.Repository}}:{{.Tag}}" \
  | grep "spring-bellsoft:0.0.1-SNAPSHOT"
239MB   spring-bellsoft:0.0.1-SNAPSHOT

bootBuildImage タスクで作成できる Docker イメージ

最初に Liberica JDK 公式ランタイムを使ってイメージサイズの小さな Docker イメージを作成する方法について説明しました。

Docker イメージのサイズを基準とすると、./gradlew bootBuildImage で作成した Docker イメージのサイズは大きいため、利用の優先度が低いと感じるかもしれません。しかし、実際の開発において、どういった Docker イメージを採用するのかは、イメージサイズのみで決定されるわけではありません。他の要素も考慮が必要なので、この Docker イメージについてもう少し詳しく説明します。

bootBuildImage タスク実行時は、Cloud Native Buildpacks を内部的に使うことを前に説明しましたが、実際に使われているビルド用の実装はデフォルトだと Paketo Buildpacks に含まれる paketo-buildpacks/bellsoft-liberica になります。これは次の URL で公開されています。

Paketo Buildpacks では、BellSoft 社が提供するバイナリ(Liberica JDK/JRE)の使用はしていますが、bellsoft/liberica-runtime-container:jdk-17-glibc などのコンテナイメージを直接ベースイメージとして使用しているわけではありません。

安全な Docker イメージを使ったサービス運用についての考え方に違いがあるため、Paketo Buildpacks と BellSoft では、コンテナイメージを構成するレイヤー設計に大きな差があります。

Paketo Buildpacks でビルドしたイメージ(bootBuildImage 使用)と、Liberica JDK 公式ランタイム(bellsoft/liberica-runtime-container:jdk-17-glibcなど)のどちらを選ぶかを決定する際の比較項目は次のようになります。

この選択は、開発・運用パイプラインの自動化を重視するか、それともイメージのサイズと起動速度の最適化を重視するかによって決まります。これは、開発プロジェクトによって違うでしょうから、方針にあわせて決めると良いでしょう。

比較項目 Paketo Buildpacks Liberica JDK 公式ランタイム
ビルドプロセス フルオートメーション Dockerfile のマニュアル管理
イメージサイズ 大きい傾向 非常に小さい
セキュリティ・パッチ 自動化しやすい 自動化しにくい
JDK/JREの選択 自動指定可能 指定が必須
最適化(起動速度) 標準的 最大化が可能
Dockerfile の有無 不要 必要
ネイティブイメージ オプションで対応可能 手動で対応可能

最初の2つだけ簡単に説明しておきます。

ビルドプロセスは、Paketo Buildpacks だとフルオートメーションなので管理が楽です。./gradlew bootBuildImage の実行のみで、依存関係、JDK、アプリケーションがコンテナ化されます。Liberica JDK 公式だと、手動設定が必要となります。自分で Dockerfile を作成し、ベースイメージ、JDK、アプリケーションのコピー、エントリポイントをすべて定義する必要があります。

イメージサイズは、Paketo Buildpacks だと大きい傾向になります。これは、共通のベース OS スタック、ビルドパックのメタデータ、起動スクリプトなどが含まれるためです。Liberica JDK 公式だと、最小限のランタイム(JRE)と OS ライブラリのみが含まれるため、非常に小さくなります。

基本は Dockerfile でカスタマイズ

本番環境で安全に実行可能な Docker コンテナを作成するには、./gradlew bootBuildImage を使用して Docker イメージを作成することが現実的な選択肢です。特にサービス開始初期においては、イメージサイズが多少大きくとも、メンテナンスの容易さを優先することが推奨されます。

しかし、開発が進んでいくと、カスタマイズした Dockerfile を使った方が楽になる場合もあります。Cloud Native Buildpacks が対応しないと、最新の Docker Engine の機能が使えないということもあったりするでしょう。

また、筆者はローカルの開発環境を充実させていくことを考慮すると、Dockerfile を使ったカスタマイズは必須という印象を持っています。

さらに、トラブルシューティングへの対応やコスト最小化のために Liberica JDK 公式ランタイムの採用も選択肢に入れることができるようにしていくことが必要だと考えています。ローカルの開発環境で使う Docker イメージの占めるディスク容量が無闇に大きくなるのも避けたいところです。

ちなみに、今回は記事執筆中に Docker Engine の v29.0 がリリースされ、これが破壊的変更を含んでいたために ./gradlew bootBuildImage が動かなくなるという問題がおきました。それに対応するため、色々調べましたが、./gradlew bootBuildImage の方は効果的な回避策が見つかりませんでした。そこで、記事では Docker in Docker フィーチャーを含む開発コンテナを用意しました。

一方の Liberica JDK 公式ランタイムを使う方式の場合は、特に問題は起きませんでした。これは、自動化部分が少ない分、環境の変化に対する影響を受けにくく、問題発生時も原因の特定や回避策の適用が比較的容易であることを示唆しています。自動化は便利ですが、ブラックボックス化が進むと、予期せぬ問題への対応が難しくなるケースがあることも、この経験から分かります。

そういったことも考慮すると、./gradlew bootBuildImage が動かなくなったときに、同等の Docker イメージを作成可能な Dockerfile を用意できるようにしておくのは意味がありそうです。

まとめ:Dockerfile と Buildpacks、賢い選択でコンテナを最適化する

本記事では、Spring Boot アプリケーションのコンテナイメージを最適化するための2つの主要なアプローチをハンズオン形式で探求しました。

  • Dockerfile とマルチステージビルドによる手動最適化
  • ./gradlew bootBuildImage による自動ビルド

Dockerfile とマルチステージビルドによる手動最適化」では、コンテナに最適化された BellSoft Liberica JDK のランタイムイメージを利用することで、イメージサイズを劇的に削減できることを確認しました。また、ビルド環境と実行環境を明確に分離することで、軽量でセキュアな本番イメージを構築できることも示しました。

./gradlew bootBuildImage による自動ビルド」では、コマンド一つで Cloud Native Buildpacks を利用したコンテナ化ができるという手軽さを学びました。こちらも内部的に Liberica JDK を利用しますが、自動化の代償としてイメージサイズは Dockerfile の場合より大きくなる傾向があります。

bootBuildImage のような自動化ツールは非常に強力ですが、Docker Engine のアップデートで動作しなくなる事例が示すように、内部がブラックボックス化されていることによるリスクも存在します。

どちらが良いという単純な話ではありません。手軽さと自動化を優先するなら Buildpacks、イメージサイズと完全なコントロールを求めるなら Dockerfile というように、プロジェクトのフェーズや要件に応じた「賢い使い分け」が重要です。

Docker を使う真の目的は、単に「環境を揃える」ことだけではなく、「本番環境に最適化された、軽量でセキュアな実行環境を、再現性高く構築する」ことです。

本記事を参考に、ぜひあなたのプロジェクトに最適なコンテナビルド戦略を見つけてください。

Discussion