我們從一開始的三高指標與同步/非同步引擎對決,一路升級到分散式環境下的防禦機制(Semaphore 背壓限流、Double-Check 、uploadId 冪等防呆、以及 Resilience4j 斷路器降級)。在本地環境下進行許多測試,證實了這套非同步 S3 微服務具備極強的抗壓能力與自癒力。
但是,一個大型的系統勢必少不了協作以及部屬到不同環境的過程,一定會遇到我電腦明明跑得好好的,為什麼部署到 QA 測試伺服器或 AWS Production 就直接炸掉????
這是因為傳統的直接部署 JAR 檔極度依賴宿主機環境:JDK 版本差異、作業系統核心不同、glibc 庫缺少、或是環境變數未設定,都會導致系統在生產環境崩潰。如果別人是用java17開發,你電腦的版本是java21有時候連編譯都不行更別說執行測試了,線上環境也是一樣,隨著程式演進越來越快,有時候舊的地端主機根本沒辦法支援太新的程式碼,但為了主機遷就版本又很難去取捨。
為了解決這些問題今天,我們正式跨入容器化部屬的階段!今天我們要把微服務從溫室般的本地 IDE 中解放出來。我們不只要將它容器化(Containerization),更要手把手教你運用 多階段建置 (Multi-stage Build) 技術,將 Docker 鏡像體積壓縮,打造出企業級生產環境規格的極輕量容器!
在進入實作前,我們先用最通俗的比喻,搞懂 Docker 與傳統虛擬機(VM)的本質差異。

補充:如果你還沒用過Docker,詳細的docker說明和安裝流程等等可以參考我之前寫的文章,詳見參考資料
# 1. 根據當前目錄的 Dockerfile 打包成鏡像 (Image)
docker build -t my-app:v1 .
# 2. 啟動容器 (背景執行 -d、連接埠對映 -p 本地埠:容器埠)
docker run -d -p 8080:8080 --name {container_name} {image_name}:v1
# 3. 檢視當前正在執行的容器清單
docker ps
# 4. 實時查看容器內部的 Console 日誌 (Log)
docker logs -f {container_name}
# 5. 進入容器內部的 Bash/Sh 互動視窗 (除錯必用)
docker exec -it {container_name} sh
# 6. 停止並強制刪除容器
docker stop {container_name} && docker rm {container_name}
Dockerfile 是一份以 YMAL 語言撰寫的 Docker 設定檔,是建立image的重要角色,而很多人寫Dockerfile 時,往往會將「編譯環境」與「執行環境」混為一談。
剛開始大家可能會用單階段寫法:產出肥大胖鏡像
FROM maven:3.9.6-eclipse-temurin-21
WORKDIR /app
COPY . .
RUN mvn package -DskipTests
CMD ["java", "-jar", "target/s3-service.jar"]
如果我們透過docker build 創建 Image 時會把整個 Maven 工具套件、全套 JDK 編譯器、Maven 本地快取(.m2 倉庫)以及原始碼,通通硬塞進了最終的生產鏡像:
為了徹底解決胖鏡像的問題,Docker 引進了 Multi-stage Build(多階段建置)技術,核心非常簡單:編譯歸編譯,執行歸執行。針對不同的程式語言架構dockerfile的內容會有些許不同,詳細可以參考官網的建議寫法
┌────────────────────────────────────────────────────────┐
│ 【Stage 1: Builder 階段】 (包含 Maven + JDK 21) │
│ · 複製 pom.xml 進行依賴下載 │
│ · 複製原始碼並執行 mvn package 編譯出 jar 檔 │
└────────────────────────────────────────────────────────┘
↓ 只需要打包好的 *.jar 檔
┌────────────────────────────────────────────────────────┐
│ 【Stage 2: Runtime 階段】 (僅包含極輕量 JRE 21 Alpine) │
│ · 僅複製 Stage 1 的 *.jar │
│ · 丟棄原始碼、丟棄 Maven、丟棄全套 JDK │
│ · 最終 Image 體積:800MB ➔ 150MB (減少 80% 體積!) │
└────────────────────────────────────────────────────────┘
現在,我們打開 Spring Boot 專案,在根目錄下新增檔案 Dockerfile,寫入生產級的多階段建置配方:
# === Stage 1: 編譯階段 (Build Stage) ===
FROM maven:3.9.6-eclipse-temurin-21-alpine AS builder
WORKDIR /build
# 技巧 1:先複製 pom.xml 並預先下載依賴 (Layer Caching)
# 只要 pom.xml 沒變,Docker 會直接重用此層快取,大幅加快後續 Build 速度!
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 複製原始碼並編譯(跳過單元測試加快打包)
COPY src ./src
RUN mvn clean package -DskipTests
# === Stage 2: 執行階段 (Runtime Stage) ===
# 技巧 2:丟棄 Maven 與 JDK!僅使用極輕量 JRE 21 Alpine 鏡像
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 建立非 Root 安全使用者(資安最佳實踐,防止容器逃逸)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# 技巧 3:COPY 時直接指定 --chown,避免額外的 chown layer 把 jar 複製兩次
COPY --from=builder --chown=appuser:appgroup /build/target/*.jar app.jar
USER appuser
EXPOSE 8080
# 優化 JVM 記憶體參數以適應容器環境
ENTRYPOINT ["java", "-XX:+UseG1GC", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
--chown 功用?Docker 的每一個 RUN / COPY 指令都會新增一個 layer,layer 一旦建立就無法刪除(只能疊加)。如果你先 COPY app.jar,再 RUN chown appuser app.jar,Docker 會在新的 layer 裡複製整個 jar 檔的 inode,導致 jar 在鏡像裡被存了兩份:
# 錯誤做法:jar 被存兩次,多佔 60MB
COPY --from=builder /build/target/*.jar app.jar # layer 1:61MB
RUN chown appuser:appgroup app.jar # layer 2:又 61MB
# 正確做法:一個指令完成,只存一次
COPY --from=builder --chown=appuser:appgroup /build/target/*.jar app.jar # 只有 61MB
docker build -t my-app:v1 .
第一次 build 需要下載依賴,之後只要 pom.xml 沒變,依賴層會直接走快取,build 速度大幅提升。
驗證鏡像體積:
docker images | grep my-app
我將前面的單階段Dockerfile建立為my-app-test:v1,多階段命名為my-app:v1列出來image後可以明顯看出兩者image體積差了一倍!
-e 帶入環境變數,將Day6產生的aws s3 金鑰代入,就可以避免敏感資料上傳到線上環境的問題啦docker run -d \
-p 8080:8080 \
--name s3-container \
-e AWS_ACCESS_KEY_ID={your_ak} \
-e AWS_SECRET_ACCESS_KEY={your_sk} \
my-app:v1
docker logs -f s3-container
本地示範時我沒有用-d 所以可以直接看到詳細console log內容

看到 Started S3ServiceApplication 代表容器內的 Spring Boot 正常啟動。
當我們成功執行docker container時,服務就已經run起來了,此時就可以使用api來測試部署好的環境有沒有成功!
例如我們想要嘗試列出檔案和上傳檔案,只要輸入對應指令就會發現系統成功回傳啦!
curl http://localhost:8080/api/s3/async/files
curl -i -X POST \
-H "uploadId: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
-F "file=@pom.xml" \
"http://localhost:8080/api/s3/async/upload/protected"

今天我們完成了系統架構演進史上重要的一步:
擺脫環境依賴:告別在我電腦可以跑別人的卻不行,環境隔離與環境即程式碼(Environment as Code)
掌握 Multi-stage Build:分離編譯與執行環境,鏡像從 1.2GB 降至 521MB,消滅了 Maven、JDK 與原始碼這三個生產環境的安全隱患。搭配 --chown 技巧避免 Docker layer 的 jar 重複問題,進一步壓縮體積。
完成容器化實戰:成功將四重防衛的非同步 S3 微服務打包並在 Docker 容器內完美運行
但是,單一容器跑起來後,新的問題又來了:當我們要對這個 Docker 容器進行高併發壓力測試時,我們該如何實時觀測容器內部的 CPU 佔用率、JVM Heap 記憶體鋸齒線、以及 Netty EventLoop 狀態?我們不能再用本地 IDE 的 JConsole 了,因為容器內部沒有 GUI 視窗!
明天,我們將引進 Docker Compose,手把手教你用一行指令拉起 App 微服務 + Prometheus 指標收集器 + Grafana 視覺化儀表板的全套企業級監控鏈路!