7.3 編寫 Dockerfile:把微服務打包為標準單元#
一個 Dockerfile 就是一份交付契約#
7.2 節有了三要素,實務問題是:如何把一個 Spring Boot 微服務變成別人 docker run 就能跑的映像?答案就是 Dockerfile——從基底、依賴、源碼到啟動命令的完整配方。
flowchart LR
SRC[源碼 + pom.xml] --> BUILD[構建階段: mvn package]
BUILD --> JAR[app.jar]
JAR --> RUNTIME[運行階段: JRE + app.jar]
RUNTIME --> IMG[最終映像]最簡版本:先跑起來#
# 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、密碼不進映像
想一想#
- 多階段構建為什麼能同時變小又變安全?builder 階段的 Maven 與源碼去哪了?
COPY . .放第一行會造成什麼緩存災難?如何用兩段 COPY 修復?- 把資料庫密碼寫進
ENV有何風險?生產上該用什麼方式注入?