iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構系列 第 16

[ Day 16 ] 容器化服務 : 撰寫 Dockerfile 與多階段建置 (Multi-stage build) 環境隔離

  • 分享至 

  • xImage
  •  

我們從一開始的三高指標與同步/非同步引擎對決,一路升級到分散式環境下的防禦機制(Semaphore 背壓限流、Double-Check 、uploadId 冪等防呆、以及 Resilience4j 斷路器降級)。在本地環境下進行許多測試,證實了這套非同步 S3 微服務具備極強的抗壓能力與自癒力。

但是,一個大型的系統勢必少不了協作以及部屬到不同環境的過程,一定會遇到我電腦明明跑得好好的,為什麼部署到 QA 測試伺服器或 AWS Production 就直接炸掉????

這是因為傳統的直接部署 JAR 檔極度依賴宿主機環境:JDK 版本差異、作業系統核心不同、glibc 庫缺少、或是環境變數未設定,都會導致系統在生產環境崩潰。如果別人是用java17開發,你電腦的版本是java21有時候連編譯都不行更別說執行測試了,線上環境也是一樣,隨著程式演進越來越快,有時候舊的地端主機根本沒辦法支援太新的程式碼,但為了主機遷就版本又很難去取捨。

為了解決這些問題今天,我們正式跨入容器化部屬的階段!今天我們要把微服務從溫室般的本地 IDE 中解放出來。我們不只要將它容器化(Containerization),更要手把手教你運用 多階段建置 (Multi-stage Build) 技術,將 Docker 鏡像體積壓縮,打造出企業級生產環境規格的極輕量容器!

快速瞭解Docker

在進入實作前,我們先用最通俗的比喻,搞懂 Docker 與傳統虛擬機(VM)的本質差異。

  1. 傳統虛擬機 (VM) vs. Docker 容器 (Container)
  • 傳統虛擬機 (VM):就像是一棟大樓裡的「獨立套房」。每一間套房都必須有自己的衛浴、廚房、隔音牆(完整的 Guest OS 核心與 Hypervisor 虛擬化層)。
    • 缺點:體積動輒 5GB ~ 20GB 起跳,啟動需要數分鐘,且極度消耗宿主機的 CPU 與記憶體。
  • Docker 容器 (Container):就像是膠囊旅館裡的「獨立床位」。所有膠囊房共享大樓的基礎設施(共享宿主機的 Linux Kernel 核心),但每個床位內部依然保有完全隔離的私人空間(獨立的檔案系統、進程空間與網路)。
    • 優勢:體積只有幾百 MB、秒級啟動、幾乎零額外效能損耗(Near-Zero Overhead)!

https://ithelp.ithome.com.tw/upload/images/20260912/20183864LIliYKazK1.png

  1. Docker 三大核心概念
    1. Dockerfile(食譜 / 配方):一個純文字檔,裡面寫滿了建置容器步驟的指令腳本。
    2. Image 鏡像(菜餚模具 / 唯讀範本):根據 Dockerfile 打包出來的唯讀套件,包含了應用程式執行所需的一切代碼、JRE、依賴庫與環境配置。
    3. Container 容器(跑起來的實體):將 Image 鏡像實體化(Run)後的獨立運行進程。

補充:如果你還沒用過Docker,詳細的docker說明和安裝流程等等可以參考我之前寫的文章,詳見參考資料

6 大 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建立Image

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 倉庫)以及原始碼,通通硬塞進了最終的生產鏡像:

  1. 部署極慢:CI/CD 流水線每次拉取極大的 Image 會嚴重卡住網路頻寬
  2. 資安漏洞(Attack Surface)大增:生產環境留著 Maven 與全套 JDK,給了攻擊者在容器內部直接編譯惡意腳本的攻擊面

多階段寫法(Multi-stage Build)

為了徹底解決胖鏡像的問題,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% 體積!)        │
└────────────────────────────────────────────────────────┘

本地實作

撰寫專屬 Dockerfile

現在,我們打開 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

打包與執行

  1. 打包 Docker 鏡像
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體積差了一倍!
https://ithelp.ithome.com.tw/upload/images/20260911/20183864JJCSImmyGS.png

  1. 啟動
    透過 -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
  1. 檢查容器日誌 (Log)
    dokcer run -d 代表背景執行,後續可以使用logs的方式查看細節
docker logs -f s3-container

本地示範時我沒有用-d 所以可以直接看到詳細console log內容

https://ithelp.ithome.com.tw/upload/images/20260912/20183864HIjLEZaPJ0.png

看到 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"

https://ithelp.ithome.com.tw/upload/images/20260912/201838647bVZTEWkT3.png

總結

今天我們完成了系統架構演進史上重要的一步:

  1. 擺脫環境依賴:告別在我電腦可以跑別人的卻不行,環境隔離與環境即程式碼(Environment as Code)

  2. 掌握 Multi-stage Build:分離編譯與執行環境,鏡像從 1.2GB 降至 521MB,消滅了 Maven、JDK 與原始碼這三個生產環境的安全隱患。搭配 --chown 技巧避免 Docker layer 的 jar 重複問題,進一步壓縮體積。

  3. 完成容器化實戰:成功將四重防衛的非同步 S3 微服務打包並在 Docker 容器內完美運行

但是,單一容器跑起來後,新的問題又來了:當我們要對這個 Docker 容器進行高併發壓力測試時,我們該如何實時觀測容器內部的 CPU 佔用率、JVM Heap 記憶體鋸齒線、以及 Netty EventLoop 狀態?我們不能再用本地 IDE 的 JConsole 了,因為容器內部沒有 GUI 視窗!

明天,我們將引進 Docker Compose,手把手教你用一行指令拉起 App 微服務 + Prometheus 指標收集器 + Grafana 視覺化儀表板的全套企業級監控鏈路!

參考資料


上一篇
[ Day 15 ] S3 癱瘓的彈性容錯 : 實作 Resilience4j 斷路器與降級(Fallback)機制
下一篇
[ Day 17 ] 容器化魔法:Docker Compose 一鍵建立 App + Prometheus + Grafana 監控服務
系列文
學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言