1素朴な Dockerfile: fat JAR を1層で入れる
の入ったに をコピーするだけの形です。これでも動きますが、依存 34 本と自分のコードが同じ1つの層(19,914,752 バイト)に入るので、コードを1行変えただけでこの層を丸ごと作り直すことになります。
FROM eclipse-temurin:25-jreWORKDIR /applicationARG JAR_FILE=out/v1/demo.jarCOPY ${JAR_FILE} application.jarENTRYPOINT ["java", "-jar", "application.jar"]
2JAR に入っている層の定義
実行可能 JAR には、どのディレクトリをどの層にするかの定義()が最初から入っています。 に付いている展開用の機能()で一覧を出すと4つの層が見え、依存()と自分のコード()が別の層に割り当てられています。変わりにくいものと変わりやすいものを分けるための区切りです。
$ java -Djarmode=tools -jar demo.jar list-layersdependenciesspring-boot-loadersnapshot-dependenciesapplication$ 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 に替えたものを使いました。
# Perform the extraction in a separate builder containerFROM bellsoft/liberica-openjre-debian:25-cds AS builderWORKDIR /builder# This points to the built jar file in the target folder# Adjust this to 'build/libs/*.jar' if you're using GradleARG JAR_FILE=target/*.jar# Copy the jar file to the working directory and rename it to application.jarCOPY ${JAR_FILE} application.jar# Extract the jar file using an efficient layoutRUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted# This is the runtime containerFROM bellsoft/liberica-openjre-debian:25-cdsWORKDIR /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 needsCOPY --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) friendlyENTRYPOINT ["java", "-jar", "application.jar"]
41行変えたあと、作り直された層はどれか
コントローラの文字列を1か所変えて JAR を作り直し、2つの方式でを作り直したときの です(上が新しい層。数字は展開後のバイト数)。素朴版は 19,914,752 バイトの層が、は 16,384 バイトの層だけが変わりました。合計はどちらも同じでイメージが小さくなったわけではなく、実際に転送される量は測っていません。
素朴版(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.jar8192 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 未満としか言えません。
ベース 展開後 圧縮後 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,644Spring native image debian:trixie-slim 196.2 61.2 257MB 42.6〜57.4 86,940〜87,160NestJS node:24-slim 294.0 83.4 377MB 143.5〜310.1 92,080〜94,556Spring JVM + AOT cache eclipse-temurin:25-jre 447.8 145.6 593MB 368.7〜423.6 285,808〜304,112Spring 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 KBSpring JVM コンパイル / クラスのロード・クラスパス走査・自動構成の判定・Bean の生成・JIT(JRE を連れて行く)