容器化技術 ch09 實戰:把 Spring Boot 專案打包成 Image
下一章→
CH 09 打包主線

實戰:把 Spring Boot 專案打包成 Image

第一版能跑的 Dockerfile多階段建置JVM 在容器裡的記憶體陷阱設定注入分層 JAR

到此為止你已經有了全部的零件知識。這一章把它們組起來,從一個普通的 Spring Boot 專案,做出一個真的能部署的映像。

作法是「一版一版改進」——先寫一個能跑但很爛的版本,然後逐一指出它的問題、逐一修好。這樣你拿到的不只是一份可以抄的 Dockerfile,而是知道每一行為什麼在那裡。

中間會特別停下來講一個 Java 專屬、而且極常見的地雷:JVM 看不到容器的記憶體限制,於是把自己撐死。(就是 ch02 那個 Exited (137)。)

假設你有一個再普通不過的 Spring Boot 專案:

myapp/
├── pom.xml
├── src/
│   └── main/
│       ├── java/com/example/myapp/MyappApplication.java
│       └── resources/application.yml
└── target/                  ← mvn package 之後才會出現
    └── myapp-0.0.1-SNAPSHOT.jar

關鍵認知:Spring Boot 的 jar 是「fat jar」

  • mvn package 產生的那個 jar 裡面已經包含了所有相依函式庫(Spring、Jackson、JDBC driver…)和一個內嵌的 Tomcat。
  • 所以執行它只需要一個 JRE:
    java -jar target/myapp-0.0.1-SNAPSHOT.jar
  • 不需要安裝 Tomcat、不需要 war 檔、不需要應用伺服器。這件事大幅簡化了容器化——容器裡只要有 Java 執行環境和這個 jar 就夠了。

所以映像裡到底要放什麼?

① 一個 JRE(不需要完整 JDK)+ ② 你的 fat jar + ③ 一行啟動指令。就這樣。

知道目標長怎樣之後,寫 Dockerfile 就只是把這三件事翻譯成指令而已。

💡 JDK 還是 JRE?JDK 含編譯器(javac)與開發工具,執行程式用不到,卻讓映像大上 150~200 MB。執行階段一律用 JRE(eclipse-temurin:17-jre)。至於編譯需要的 JDK,等一下用多階段建置解決——編譯用完就丟,不進最終映像。

先做出一個能動的。在專案根目錄建立 Dockerfile:

# === 第一版:可以跑,但很多問題(等一下逐一修) ===
FROM eclipse-temurin:17-jre-jammy

WORKDIR /app

COPY target/myapp-0.0.1-SNAPSHOT.jar app.jar

EXPOSE 8080

CMD ["java", "-jar", "app.jar"]

逐行解讀(每一行都對應到前面某一章)

  • FROM eclipse-temurin:17-jre-jammy:以 Eclipse Temurin 的 JRE 17 為基底(jammy = Ubuntu 22.04)。用明確版本而不是 latest(ch04)。
  • WORKDIR /app:設定工作目錄,之後的相對路徑以此為準(ch08)。
  • COPY ...:把本機 target/ 下的 jar 複製進映像並改名 app.jar。改名是為了避免 Dockerfile 寫死版本號(不然每次改版本都要改 Dockerfile)。
  • EXPOSE 8080:文件宣告,實際要靠 -p(ch07)。
  • CMD [...]:exec form,讓 java 成為 PID 1,SIGTERM 收得到(ch05)。

建置與執行

# ① 先在本機打包(第一版的缺點之一,等下會解決)
mvn clean package -DskipTests

# ② 建置映像
docker build -t myapp:1.0 .

# ③ 執行
docker run -d --name myapp -p 8080:8080 myapp:1.0

# ④ 確認
docker logs -f myapp
curl http://localhost:8080/actuator/health

# ⑤ 進去看看(觀察,不要改東西)
docker exec -it myapp bash

# ⑥ 清掉
docker rm -f myapp

它現在有五個問題

  1. 依賴本機環境:必須先在自己電腦上 mvn package。同事沒裝 Maven、或 JDK 版本不同,結果就不一樣——ch01 那個「在我電腦上可以跑」又回來了。
  2. 映像偏大:JRE 基底約 190 MB + 你的 fat jar。可以更小。
  3. 快取效率差:jar 是單一檔案,改一行程式碼整個 jar 的雜湊就變了,那一層完全重建、重新推送(ch08)。
  4. 以 root 執行:預設身分是 root,一旦被入侵風險極高(ch02、ch10)。
  5. JVM 不知道自己被關在容器裡:記憶體設定可能出大事。這是下一張卡的主題。

這是 Java 開發者最該懂、卻最少人講清楚的一段。

問題的根源

  • JVM 啟動時會自動決定 heap 的最大值(沒指定 -Xmx 時預設約為「可見記憶體的 1/4」)。
  • 「可見記憶體」怎麼來的?舊版 JVM 是去讀「這台機器有多少記憶體」——於是它看到宿主機的 32 GB,決定 heap 上限 8 GB。
  • 但你的容器 cgroup 只給了 512 MB(ch02)。
災難鏈:JVM 以為自己有 8 GB 可以用,於是放心地讓 heap 長大 → 超過 512 MB → 核心的 OOM Killer 直接 SIGKILL → 容器 Exited (137),docker inspect 顯示 OOMKilled: true。

更糟的是:這不是 Java 的 OutOfMemoryError——JVM 是被外面殺掉的,連 stack trace 都印不出來。你只會看到應用毫無徵兆地消失。

好消息:現代 JVM 已經看得懂 cgroup

  • JDK 10 起(並回溯到 8u191+)內建容器感知,會去讀 cgroup 限制而不是宿主機記憶體。
  • 你用 JDK 17 的話,這個問題預設已經解決。可以驗證:
    docker run --rm -m 512m eclipse-temurin:17-jre java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
    # 應該顯示約 128 MB(512 的 1/4),而不是宿主機的 1/4

但預設值仍然不夠好:1/4 太保守

容器裡通常只跑你一個應用,把 75% 留著不用太浪費。正確作法是用比例而非固定值:

ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
🚨 為什麼用 MaxRAMPercentage 而不是寫死 -Xmx384m?
因為寫死的值會跟著映像跑到每一個環境。同一個映像在開發機給 512 MB、在正式環境給 4 GB,寫死 384m 的話正式環境就浪費了 90% 的記憶體。用百分比,映像自動適應它被分配到的資源——這才是雲原生的做法(K8s 環境尤其重要,ch14)。

還有一個常被忽略的重點:heap 不等於容器記憶體用量

JVM 除了 heap 還要用:metaspace(類別資訊)、執行緒堆疊(每條約 1 MB)、JIT 編譯的程式碼快取、直接記憶體(NIO buffer)、GC 自己的結構。

  • 所以 MaxRAMPercentage=75 留 25% 給這些「非 heap」開銷,是經驗上合理的起點。
  • 設成 100 保證會被 OOMKilled。
  • 容器記憶體上限也不要開太小:Spring Boot 應用實務上建議至少 512 MB,小於這個常常啟動就掛。

排查流程

docker inspect myapp --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
#  true 137  → 確定是被 OOM Killer 殺的

docker stats myapp        # 即時看「已用 / 上限」

# 讓 JVM 在真的 OOM 時留下證據
ENV JAVA_OPTS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof"
docker cp myapp:/tmp/heapdump.hprof ./     # 撈出來分析

解決第一版的問題 ①(依賴本機 Maven)與 ②(映像大)。核心概念:在映像裡編譯,但編譯環境不進最終映像。

# syntax=docker/dockerfile:1

# ========== 第 1 階段:建置(用完就丟)==========
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build

# 先只複製 pom.xml,讓依賴下載可以被快取(ch08 第一設計原則)
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline -B

# 再複製原始碼(常變動的放後面)
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests -B

# ========== 第 2 階段:執行(這才是最終映像)==========
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app

# 建立非 root 使用者(ch10 會深入)
RUN groupadd -r spring && useradd -r -g spring spring

# 只從建置階段複製「成品」,Maven、原始碼、.m2 通通不會進來
COPY --from=builder --chown=spring:spring /build/target/*.jar app.jar

USER spring
EXPOSE 8080

ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]

關鍵在 COPY --from

COPY --from=builder /build/target/*.jar app.jar 是整個技巧的核心:只把第一階段的「成品」複製過來。第一階段那個含 Maven、JDK、所有原始碼、200 個下載的 jar 的環境(大約 800 MB)完全不會出現在最終映像裡。

四個立刻拿到的好處

  1. 任何有 Docker 的機器都能建置,不必先裝 Maven 或特定 JDK。ch01 的環境一致性,這次連建置過程也涵蓋了。
  2. 最終映像只有 JRE + 你的 jar(約 200 MB),而不是 800 MB。
  3. CI 友善:`docker build` 一行搞定,不需要在 CI 裡另外配置 Java 環境。
  4. 安全:原始碼、建置工具、Maven 憑證都不在最終映像裡——攻擊面小很多。

那個 ENTRYPOINT 為什麼要用 sh -c?

  • 因為要展開 $JAVA_OPTS 這個變數,而 exec form 不經過 shell 就不會展開。
  • 但注意前面加了 exec——這讓 java 取代 shell、繼承 PID 1,訊號才收得到(ch05、ch08)。少了這個 exec,前面所有的優雅關機設定都白費。
  • 如果你不需要動態調整 JVM 參數,最乾淨的寫法還是純 exec form:ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "/app/app.jar"]。

鐵律:映像只有一份,環境差異全部靠外部注入。絕對不要為 dev / test / prod 各建一個映像——那樣你正式環境跑的,就不是你測過的那個東西。

Spring Boot 的環境變數對應規則(relaxed binding)

Spring Boot 會自動把環境變數對應到設定屬性,規則很單純:點改成底線、全部大寫。

spring.datasource.url        →  SPRING_DATASOURCE_URL
spring.datasource.username   →  SPRING_DATASOURCE_USERNAME
spring.jpa.hibernate.ddl-auto →  SPRING_JPA_HIBERNATE_DDLAUTO   (連字號直接去掉)
server.port                  →  SERVER_PORT
docker run -d \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e SPRING_DATASOURCE_URL='jdbc:sqlserver://sqlserver:1433;databaseName=mydb' \
  -e SPRING_DATASOURCE_USERNAME=sa \
  -e SPRING_DATASOURCE_PASSWORD="$DB_PASSWORD" \
  myapp:1.0
💡 注意連線位址寫的是 sqlserver:1433 不是 localhost——ch07 那個頭號錯誤。

設定的優先順序(高到低)

  1. 指令列參數(--spring.datasource.url=...)
  2. 環境變數 ← 容器化的主要管道
  3. 外部設定檔(掛進去的 application.yml)
  4. jar 內的 application-{profile}.yml
  5. jar 內的 application.yml(只放各環境共通的預設值)

密碼絕對不要用這三種方式

  • ❌ 寫在 application.yml 裡打包進 jar(永久留在映像層,ch03)。
  • ❌ 寫在 Dockerfile 的 ENV(docker inspect 就看得到)。
  • ❌ 寫死在 docker-compose.yml(這份檔案會進版控)。

正確作法(由簡到嚴謹):

  • 開發:.env 檔(務必加進 .gitignore)+ compose 的 ${VAR} 展開。
  • 正式:容器平台的 secret 機制(Docker Swarm secrets、K8s Secret)或雲端服務(AWS Secrets Manager、Parameter Store,見 AWS ch10)。

日誌與時區兩個小坑

  • 日誌印到 stdout(ch05):Spring Boot 預設就是,不要去設定 file appender。
  • 時區:容器預設 UTC,你的資料庫時間會差 8 小時。
    ENV TZ=Asia/Taipei
    (更嚴謹的做法是全系統統一用 UTC,只在顯示層轉換。)

解決第一版的問題 ③(快取效率差)。這是 Spring Boot 官方提供的進階技巧。

問題回顧

fat jar 是單一檔案,裡面 95% 是不常變的第三方函式庫、5% 是你的程式碼。但 Docker 的層是以檔案為單位——你改一行程式碼,整個 60 MB 的 jar 雜湊就變了,那一層完全重建、推送時整個 60 MB 重傳。

解法:Spring Boot 的 layertools 把 jar 拆成四層

java -Djarmode=layertools -jar app.jar list

dependencies           ← 第三方函式庫(幾乎不變,最大)
spring-boot-loader     ← Spring Boot 載入器(幾乎不變)
snapshot-dependencies  ← SNAPSHOT 版依賴(偶爾變)
application            ← 你自己的程式碼與資源(每次都變,最小)

照這個順序複製到映像,每一個變成獨立的層——於是改程式碼時,只有最後那個幾 MB 的 application 層需要重建與推送。

最終版 Dockerfile(可以直接拿去用)

# syntax=docker/dockerfile:1

# ========== 階段 1:編譯 ==========
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline -B
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests -B

# ========== 階段 2:拆解 fat jar ==========
FROM eclipse-temurin:17-jre-jammy AS extractor
WORKDIR /extract
COPY --from=builder /build/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

# ========== 階段 3:最終映像 ==========
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app

RUN groupadd -r spring && useradd -r -g spring spring

# 依「變動頻率由低到高」逐層複製 —— ch08 的第一設計原則
COPY --from=extractor --chown=spring:spring /extract/dependencies/ ./
COPY --from=extractor --chown=spring:spring /extract/spring-boot-loader/ ./
COPY --from=extractor --chown=spring:spring /extract/snapshot-dependencies/ ./
COPY --from=extractor --chown=spring:spring /extract/application/ ./

USER spring
EXPOSE 8080
ENV TZ=Asia/Taipei

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

# 注意:分層之後不能再用 -jar,要用 Spring Boot 的啟動類別
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", \
            "org.springframework.boot.loader.launch.JarLauncher"]
🚨 兩個容易出錯的細節:
① 分層之後不能用 java -jar app.jar(jar 已經被拆開了),要用 JarLauncher。Spring Boot 3.2 起類別路徑是 org.springframework.boot.loader.launch.JarLauncher,3.1 以前是 org.springframework.boot.loader.JarLauncher(少了 .launch)——寫錯會噴 ClassNotFoundException。
② HEALTHCHECK 的 --start-period=60s 一定要留,Spring Boot 冷啟動很慢(ch05)。
⭐ 成果:改一行程式碼重新建置,只有最後一層(幾 MB)變動,其餘全部命中快取。推送到 registry 時也只傳那幾 MB——在 CI/CD 每天跑幾十次的情境下,這個差別非常有感。
三個版本演進對照
面向第一版(單階段)第二版(多階段)第三版(分層 JAR)
需要本機裝 Maven要不用不用
最終映像大小約 250 MB約 250 MB(建置環境不進來)約 250 MB(層分得更細)
改一行程式碼要重建什麼整個 jar 那層(60 MB)整個 jar 那層(60 MB)只有 application 層(幾 MB)
重新下載 Maven 依賴本機自己處理不用(pom 先 COPY + cache mount)不用
以什麼身分執行root ⚠️非 root(spring)非 root(spring)
推送到 registry 的量整層 60 MB整層 60 MB只有幾 MB
適合第一次理解流程多數專案的合理起點CI/CD 高頻部署
JVM 容器化必備參數
參數作用建議值為什麼
-XX:MaxRAMPercentageheap 上限佔容器記憶體的比例75.0留 25% 給 metaspace、執行緒堆疊、JIT 快取等非 heap 開銷
-XX:InitialRAMPercentage初始 heap 比例50.0減少啟動期間反覆擴張 heap 的開銷
-Xmx(寫死)固定 heap 上限避免使用寫死的值會跟著映像跑到每個環境,無法自動適應配額
-XX:+HeapDumpOnOutOfMemoryErrorOOM 時自動產生 heap dump開啟留下現場證據,配合 docker cp 撈出來分析
容器記憶體上限(-m)cgroup 限制Spring Boot ≥ 512 MB太小會啟動即掛;JDK 10+ 會自動讀取這個值

練習題 點選選項查看解析

0 / 9
01 / 9
為什麼容器化 Spring Boot 應用時,執行階段用 JRE 而不是 JDK?
A JRE 執行速度比較快
B JDK 含編譯器與開發工具,執行 fat jar 完全用不到,卻讓映像多 150~200 MB;編譯所需的 JDK 交給多階段建置的第一階段,用完就丟
C JDK 不能在容器裡執行
D JRE 比較安全,因為不能寫程式
解析
Spring Boot 的 fat jar 已包含所有依賴與內嵌 Tomcat,執行只需要 java 指令。多階段建置讓你兩者兼得:第一階段用完整的 maven+JDK 映像編譯,第二階段只從裡面複製出成品 jar,建置環境完全不進最終映像。
02 / 9
容器裡的 Java 應用毫無徵兆消失,docker inspect 顯示 OOMKilled: true、ExitCode 137。最可能的原因是?
A 程式碼有記憶體洩漏,拋出了 OutOfMemoryError
B JVM 使用的記憶體超過 cgroup 限制,被核心的 OOM Killer 用 SIGKILL 殺掉——這不是 Java 的 OOM,所以連 stack trace 都印不出來
C 磁碟空間不足
D 網路連線中斷
解析
關鍵區別:Java 的 OutOfMemoryError 是 JVM 自己發現 heap 不夠而拋出的例外(有 stack trace);OOMKilled 是外面的核心把整個 process 砍掉(什麼都來不及印)。舊版 JVM 會去讀宿主機的總記憶體而非 cgroup 限制,於是把 heap 設得遠超容器配額。JDK 10+(含 8u191+)已內建容器感知。
03 / 9
為什麼建議用 -XX:MaxRAMPercentage=75.0 而不是寫死 -Xmx384m?
A 百分比寫法效能較好
B 寫死的值會跟著映像跑到每個環境——同一個映像在開發機給 512 MB、正式環境給 4 GB,寫死 384m 會浪費 90% 的記憶體;用比例可自動適應被分配到的資源
C -Xmx 參數已被淘汰
D 百分比可以突破 cgroup 限制
解析
「同一個映像跑遍所有環境」是容器化的核心原則,寫死記憶體值直接違反它。留 25% 給非 heap 開銷(metaspace、執行緒堆疊、JIT 程式碼快取、NIO 直接記憶體、GC 結構)是經驗上合理的起點——設成 100 保證會被 OOMKilled。
04 / 9
多階段建置中 COPY --from=builder /build/target/*.jar app.jar 的意義是?
A 從另一個映像下載 jar 檔
B 只把第一階段的「成品」複製到最終映像;那個含 Maven、JDK、原始碼、數百個下載 jar 的建置環境(約 800 MB)完全不會出現在最終映像裡
C 把 jar 檔備份到 builder 階段
D 在兩個階段間共用檔案系統
解析
這是多階段建置的核心技巧,一次拿到四個好處:任何有 Docker 的機器都能建置(不必裝 Maven)、映像小、CI 友善、且原始碼與建置憑證不會留在最終映像裡(攻擊面小很多)。
05 / 9
Spring Boot 中,環境變數 SPRING_DATASOURCE_URL 對應到哪個設定屬性?
A spring-datasource-url
B spring.datasource.url(relaxed binding:點改成底線、全部大寫)
C SPRING.DATASOURCE.URL
D 需要在 application.yml 手動宣告對應
解析
規則很單純:點改底線、全大寫、連字號直接去掉(spring.jpa.hibernate.ddl-auto → SPRING_JPA_HIBERNATE_DDLAUTO)。環境變數的優先序高於 jar 內的設定檔,這正是「同一個映像跑遍所有環境」的實現方式。
06 / 9
下列哪一種放置資料庫密碼的方式是安全的?
A 寫在 application.yml 裡打包進 jar
B 執行時透過環境變數或容器平台的 secret 機制注入(開發用 .env 並加進 .gitignore,正式用 K8s Secret 或 AWS Secrets Manager)
C 寫在 Dockerfile 的 ENV 指令
D 寫死在 docker-compose.yml
解析
前三個都會讓密碼永久留下:打包進 jar 就留在映像層(ch03 說過刪不掉)、ENV 用 docker inspect 就看得到、compose 檔會進版控。唯一安全的方向是「執行時才注入」,且注入來源本身要受保護。
07 / 9
Spring Boot 的分層 JAR(layertools)解決什麼問題?
A 讓應用啟動更快
B fat jar 是單一檔案,改一行程式碼整個 60 MB 的層就要重建與重傳;拆成 dependencies / loader / snapshot / application 四層後,改程式碼只需重建最後幾 MB 的那層
C 減少記憶體使用
D 支援多個應用共用同一個 jar
解析
這是 ch08「變動頻率低的放前面」原則做到極致:把 95% 不常變的第三方函式庫和 5% 常變的自家程式碼分開成獨立的層。在 CI/CD 每天跑幾十次的情境下,建置時間與推送流量的差別非常有感。
08 / 9
使用分層 JAR 之後,ENTRYPOINT 為什麼不能再寫 java -jar app.jar?
A 因為分層後檔名改變了
B 因為 jar 已經被 extract 拆開成目錄結構,不再是一個可執行的 jar;必須改用 Spring Boot 的 JarLauncher 啟動類別
C 因為 -jar 參數不支援多層映像
D 因為需要指定 classpath
解析
要用 org.springframework.boot.loader.launch.JarLauncher(Spring Boot 3.2+)或 org.springframework.boot.loader.JarLauncher(3.1 以前,少了 .launch)。版本寫錯會噴 ClassNotFoundException,是這個技巧最常見的卡關點。
09 / 9
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"] 中的 exec 為什麼不能省略?
A 為了讓變數正確展開
B exec 讓 java 取代這個 shell 而非成為子行程,於是 java 繼承 PID 1、SIGTERM 收得到;少了它,前面所有優雅關機的設定都會失效
C 為了提升啟動速度
D 為了讓 JAVA_OPTS 生效
解析
用 sh -c 是為了展開 $JAVA_OPTS(exec form 不經過 shell 就不會展開),但代價是引入了一個 shell。加上 exec 就能兩全其美。如果不需要動態調整 JVM 參數,最乾淨的還是純 exec form 陣列寫法。

點擊卡片翻面查看答案,共 11 張。

QUESTION
Spring Boot 映像裡只需要放什麼?
點擊翻面
ANSWER
① 一個 JRE(不要 JDK)
② 你的 fat jar(已含所有依賴與內嵌 Tomcat)
③ 一行啟動指令
編譯用的 JDK/Maven 交給多階段建置,用完就丟
點擊翻回
QUESTION
第一版 Dockerfile 的五個問題?
點擊翻面
ANSWER
① 依賴本機先 mvn package
② 映像偏大
③ 改一行程式碼整個 jar 層重建
④ 以 root 執行
⑤ JVM 不知道 cgroup 限制
點擊翻回
QUESTION
OOMKilled 與 Java OutOfMemoryError 的差別?
點擊翻面
ANSWER
OOMKilled:核心從外面 SIGKILL,Exit 137,什麼都印不出來
OutOfMemoryError:JVM 自己拋的例外,有 stack trace
docker inspect 看 .State.OOMKilled 可確認
點擊翻回
QUESTION
為什麼用 MaxRAMPercentage 而非 -Xmx?
點擊翻面
ANSWER
寫死的值會跟著映像跑到每個環境(512MB 開發機 / 4GB 正式)
用比例讓映像自動適應被分配到的資源
建議 75%,留 25% 給非 heap 開銷
點擊翻回
QUESTION
heap 之外 JVM 還吃什麼記憶體?
點擊翻面
ANSWER
metaspace(類別資訊)、執行緒堆疊(每條約 1MB)
JIT 程式碼快取、NIO 直接記憶體、GC 結構
→ 所以 MaxRAMPercentage 設 100 保證被 OOMKilled
點擊翻回
QUESTION
多階段建置的核心是哪一行?
點擊翻面
ANSWER
COPY --from=builder /build/target/*.jar app.jar
只複製「成品」,800MB 的建置環境完全不進最終映像
好處:不用本機裝 Maven、映像小、CI 友善、攻擊面小
點擊翻回
QUESTION
Spring Boot 環境變數對應規則?
點擊翻面
ANSWER
點改底線、全大寫、連字號去掉
spring.datasource.url → SPRING_DATASOURCE_URL
spring.jpa.hibernate.ddl-auto → SPRING_JPA_HIBERNATE_DDLAUTO
點擊翻回
QUESTION
密碼絕對不能放的三個地方?
點擊翻面
ANSWER
❌ application.yml 打包進 jar(永久留在映像層)
❌ Dockerfile 的 ENV(docker inspect 看得到)
❌ 寫死在 compose 檔(會進版控)
✅ 執行時注入:.env(gitignore)/K8s Secret/Secrets Manager
點擊翻回
QUESTION
分層 JAR 的四層與順序?
點擊翻面
ANSWER
dependencies → spring-boot-loader → snapshot-dependencies → application
由「變動頻率低到高」排列
改程式碼只需重建最後幾 MB 的 application 層
點擊翻回
QUESTION
分層後的啟動類別是什麼?
點擊翻面
ANSWER
不能再用 java -jar(jar 已被拆開)
Spring Boot 3.2+:org.springframework.boot.loader.launch.JarLauncher
3.1 以前:org.springframework.boot.loader.JarLauncher(少 .launch)
點擊翻回
QUESTION
容器化 Spring Boot 的兩個小坑?
點擊翻面
ANSWER
① 日誌印 stdout 就好,不要設 file appender(ch05)
② 容器預設 UTC → ENV TZ=Asia/Taipei,否則時間差 8 小時
點擊翻回