從多階段構建到 JVM 參數,優化 Java 容器體質。
環境一致性是 CI/CD 的基石。對 Java 應用來說,不只要顧編譯產物 (JAR),還要顧 JVM 版本跟資源限制。「在我電腦上可以跑啊」這句工程師名言,就讓 Docker 來終結它吧。
Java 容器化最大的坑就是 Image 太肥:Maven + JDK + 原始碼全部打包進去,動輒 800MB 起跳,推個 Image 都要等到天荒地老。解法是多階段構建——用 maven:3.9-eclipse-temurin-21 編譯,最後只把 JAR 複製進輕量的 eclipse-temurin:21-jre 運行環境。
這是 services/user-service/Dockerfile 的實際內容(三個 Stage):
# ---- Build Stage ----
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# 先只複製 pom.xml 下載依賴(善用 Docker Layer 快取)
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 再複製原始碼進行編譯
COPY src/ ./src/
RUN mvn package -DskipTests -B
# ---- Agent Downloader ----
FROM alpine:latest AS agent-downloader
WORKDIR /agent
RUN apk add --no-cache curl
RUN curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar -o opentelemetry-javaagent.jar
# ---- Production Stage ----
FROM eclipse-temurin:21-jre
WORKDIR /app
# 安全性:以非 Root 用戶運行
RUN groupadd --gid 1001 appgroup && \
useradd --uid 1001 --gid appgroup --shell /bin/sh appuser
COPY --from=builder /app/target/*.jar app.jar
COPY --from=agent-downloader /agent/opentelemetry-javaagent.jar .
USER appuser
三個重點:
COPY pom.xml 與 COPY src/ 分開:只要 pom.xml 沒變,mvn dependency:go-offline 這層就會命中快取,改一行程式碼不用重新下載整個 Maven 倉庫。USER appuser 是容器安全的基本功,之後 K8S 的 SecurityContext 也會感謝你。第一次建置時,Maven 要下載所有依賴,我去泡了杯咖啡回來還在跑。那第二次建置(程式碼沒變)呢?直接看終端機輸出:

▲ docker build 第二次建置:所有 Layer 命中 CACHED,整體只花 0.1 秒
所有步驟都顯示 CACHED,連最重的 mvn dependency:go-offline 跟 mvn package 都是,整個建置 1 秒內結束。第一次看到的時候真的只有一個感想:太神啦。
這就是為什麼 Dockerfile 的指令順序這麼重要——變動頻率低的層(依賴下載)放前面,變動頻率高的層(原始碼)放後面,快取才吃得到。
UseContainerSupport(預設開啟),會根據容器的 cgroup 限制自動計算 Heap 大小。Java 21 在這塊已經很成熟。requests/limits 跟 -XX:MaxRAMPercentage 怎麼搭配,Day 28 效能調優會深入聊。今天我們把 Java 服務裝進了容器:多階段構建控制 Image 大小、Layer 快取把重建時間壓到秒級。明天輪到 Python——FastAPI + Delta Lake 的大數據容器,那邊有一個讓我卡了一陣子的 Schema 坑等著各位,敬請期待(?)。