跳轉到

7.3 編寫 Dockerfile:把微服務打包為標準單元#

一個 Dockerfile 就是一份交付契約#

7.2 節有了三要素,實務問題是:如何把一個 Spring Boot 微服務變成別人 docker run 就能跑的映像?答案就是 Dockerfile——從基底、依賴、源碼到啟動命令的完整配方。

最簡版本:先跑起來#

# Dockerfile.v1:能用,但很肥(約 700MB)
FROM openjdk:17
WORKDIR /app
COPY target/shop-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# 構建並運行(在 Spring Boot 專案根目錄執行)
mvn -DskipTests package
docker build -f Dockerfile.v1 -t shop-service:v1 .
docker run -d --name shop -p 8080:8080 shop-service:v1
docker logs -f shop

問題:openjdk:17 含完整 JDK 與 Debian,生產只需要 JRE;且源碼、Maven 緩存全混進映像。

多階段構建:編譯與運行徹底分離#

# Dockerfile:多階段,可直接用於生產
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml ./
RUN mvn -B dependency:go-offline
COPY src ./src
RUN mvn -B -DskipTests package

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /build/target/shop-service.jar app.jar
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
# 構建多階段版本,對比體積
docker build -t shop-service:v2 .
docker images shop-service
docker run -d --name shop2 -p 8081:8080 shop-service:v2
curl localhost:8081/actuator/health
手法 效果
builder + runtime 分離 最終映像不含 Maven、源碼,體積縮小 70%
dependency:go-offline 先行 pom 未變則依賴層命中緩存
alpine JRE 基底 比 JDK 減約 500MB
非 root USER app 容器逃逸傷害大幅降低

.dockerignore:別把垃圾打包進去#

# .dockerignore:與 .gitignore 類似,可直接使用
.git
.idea
target/*.original
*.md
docker-compose*.yml
.gitignore
Dockerfile*
# 驗證忽略是否生效:對比構建上下文大小
docker build --help | grep -i context
tar -c . --exclude-vcs | wc -c

沒有 .dockerignore,每次 COPY . . 會把 .git(可能數百 MB)也送進 Docker daemon,緩存全失效。

最佳實踐清單#

原則 做法 反例
層越少越好但可讀優先 合併 apt-get update && install && rm -rf 每個命令一行,殘留 apt 緩存
敏感資訊不進映像 用 --secret 或運行時環境變數 ENV DB_PASSWORD=123456
版本釘死 FROM eclipse-temurin:17.0.9_9-jre-alpine FROM openjdk:latest
健康檢查內建 HEALTHCHECK CMD curl -f /actuator/health 靠人工 docker ps 猜活死
優雅停機 ENTRYPOINT 用 exec 形式 用 shell 形式吃掉 SIGTERM
# 收尾:在最終階段追加健康檢查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health | grep UP || exit 1
# 安全掃描與檢查,一鍵執行
docker scout quickview shop-service:v2 || trivy image shop-service:v2
docker inspect shop-service:v2 --format='{{.Config.User}} {{.Config.ExposedPorts}}'

本章小結#

  • Dockerfile 是交付契約:基底、依賴、啟動方式全部程式碼化
  • 多階段構建:builder 編譯、runtime 只帶 JRE + jar
  • 緩存鐵律:pom 先行、源碼最後,非必要不放 COPY . . 在前
  • .dockerignore 是標配,否則上下文肥大拖慢構建
  • 生產四件套:釘版本、非 root、HEALTHCHECK、密碼不進映像

想一想#

  1. 多階段構建為什麼能同時變小又變安全?builder 階段的 Maven 與源碼去哪了?
  2. COPY . . 放第一行會造成什麼緩存災難?如何用兩段 COPY 修復?
  3. 把資料庫密碼寫進 ENV 有何風險?生產上該用什麼方式注入?