Dev Study
← 解説「コンテナイメージと3者の比較」に戻る

サンプルコードで身につける: コンテナイメージと3者の比較

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1素朴な Dockerfile: fat JAR を1層で入れる

の入ったに をコピーするだけの形です。これでも動きますが、依存 34 本と自分のコードが同じ1つの層(19,914,752 バイト)に入るので、コードを1行変えただけでこの層を丸ごと作り直すことになります。

Dockerfile(素朴版。計測に使ったもの)設定ファイル
FROM eclipse-temurin:25-jre
WORKDIR /application
ARG JAR_FILE=out/v1/demo.jar
COPY ${JAR_FILE} application.jar
ENTRYPOINT ["java", "-jar", "application.jar"]

2JAR に入っている層の定義

実行可能 JAR には、どのディレクトリをどの層にするかの定義()が最初から入っています。 に付いている展開用の機能()で一覧を出すと4つの層が見え、依存()と自分のコード()が別の層に割り当てられています。変わりにくいものと変わりやすいものを分けるための区切りです。

java -Djarmode=tools -jar demo.jar list-layers と、JAR 内の BOOT-INF/layers.idx(Boot 4.1.1。unzip の行は再構成)コマンド出力
$ java -Djarmode=tools -jar demo.jar list-layers
dependencies
spring-boot-loader
snapshot-dependencies
application
 
$ unzip -p demo.jar BOOT-INF/layers.idx
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"

3リファレンスの Dockerfile: 層ごとに COPY する

4.1 のリファレンスに載っている です(コメントも原文のまま)。前半で JAR を層ごとのディレクトリに展開し、後半で COPY を4回に分けて、それぞれを の層にしています。計測では、他と JRE をそろえるためにベースイメージだけを eclipse-temurin:25-jre に替えたものを使いました。

Dockerfile(Spring Boot 4.1 リファレンス「Dockerfiles」より)設定ファイル
# Perform the extraction in a separate builder container
FROM bellsoft/liberica-openjre-debian:25-cds AS builder
WORKDIR /builder
# This points to the built jar file in the target folder
# Adjust this to 'build/libs/*.jar' if you're using Gradle
ARG JAR_FILE=target/*.jar
# Copy the jar file to the working directory and rename it to application.jar
COPY ${JAR_FILE} application.jar
# Extract the jar file using an efficient layout
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted
 
# This is the runtime container
FROM bellsoft/liberica-openjre-debian:25-cds
WORKDIR /application
# Copy the extracted jar contents from the builder container into the working directory in the runtime container
# Every copy step creates a new docker layer
# This allows docker to only pull the changes it really needs
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
# Start the application jar - this is not the uber jar used by the builder
# This jar only contains application code and references to the extracted jar files
# This layout is efficient to start up and AOT cache (and CDS) friendly
ENTRYPOINT ["java", "-jar", "application.jar"]

41行変えたあと、作り直された層はどれか

コントローラの文字列を1か所変えて JAR を作り直し、2つの方式でを作り直したときの です(上が新しい層。数字は展開後のバイト数)。素朴版は 19,914,752 バイトの層が、は 16,384 バイトの層だけが変わりました。合計はどちらも同じでイメージが小さくなったわけではなく、実際に転送される量は測っていません。

docker history を「展開後のバイト数と、その層を作った命令」の形式で出したもの(ベースイメージの層は ... で省略。見出しと「←」は注記)コマンド出力
素朴版(8層。v1 → v2 で変わった層は1つ)
0 ENTRYPOINT ["java" "-jar" "application.jar"]
19914752 COPY out/v1/demo.jar application.jar ← 変わったのはここ
0 ARG JAR_FILE=out/v1/demo.jar
8192 WORKDIR /application
...
 
レイヤ分け(11層。v1 → v2 で変わった層は1つ)
0 ENTRYPOINT ["java" "-jar" "application.jar"]
16384 COPY /builder/extracted/application/ ./ ← 変わったのはここだけ
4096 COPY /builder/extracted/snapshot-dependencies/ ./
4096 COPY /builder/extracted/spring-boot-loader/ ./
19783680 COPY /builder/extracted/dependencies/ ./
8192 WORKDIR /application
...

5最終比較: 何を成果物に入れ、何を実行時に回したか

サイズは測り方で変わるので3つの指標を並べました(同じイメージでも docker images の表示は圧縮後の約4倍です)。上の行ほどビルド時に済ませた仕事が多く、下の行ほど実行系を連れて行って起動のたびに仕事をしています。アイドル時の値であり、負荷時の性能やメモリは測っておらず、Rust の ready は計測の分解能に近いので 10 ms 未満としか言えません。

最終イメージの比較(各7回の範囲。Docker 再起動後の系列。arm64・メモリ上限なし。サイズは MB = 10^6 バイト)表
ベース 展開後 圧縮後 docker images ready(ms) アイドル RSS(KB)
(history) (inspect) の表示
Rust(axum) debian:trixie-slim 111.5 30.9 142MB 4.7〜9.6 2,372〜2,644
Spring native image debian:trixie-slim 196.2 61.2 257MB 42.6〜57.4 86,940〜87,160
NestJS node:24-slim 294.0 83.4 377MB 143.5〜310.1 92,080〜94,556
Spring JVM + AOT cache eclipse-temurin:25-jre 447.8 145.6 593MB 368.7〜423.6 285,808〜304,112
Spring JVM(レイヤ分け) eclipse-temurin:25-jre 395.7 132.7 528MB 693.8〜844.1 264,996〜283,088
 
ビルド時に済ませたこと / 起動のたびにやること
Rust 依存のコンパイルと結合まで / OS が実行ファイルを読むだけ
Spring native image クラスパス走査・自動構成の判定・要るコードの解析まで(ビルド 66〜68 秒・ピーク 4.18〜4.29GB)/ JRE は持たない
NestJS 型検査と JS への変換 / モジュールの読み込み・DI の配線・JIT(依存は成果物の外)
Spring JVM + AOT cache 下に加えてクラスのロードとリンクを前回の実行から持ち越す(+52MB。イメージのビルド中に学習用の実行をした分)/ 残りは下と同じ
 
※ JVM の RSS は既定のヒープ上限に左右される。--memory=512m を付けると Spring JVM(レイヤ分け)は 195,828〜201,384 KB
Spring JVM コンパイル / クラスのロード・クラスパス走査・自動構成の判定・Bean の生成・JIT(JRE を連れて行く)

図やコードに出てくる用語(タップで説明)