给出一个 Java Maven 项目的两阶段 Dockerfile 并说明每阶段职责

解读

国内面试官问“两阶段 Dockerfile”时,真正想考察的是你对“构建阶段与运行阶段彻底分离”这一最佳实践的理解深度
他们不仅希望看到“能跑起来”的镜像,更在意:

  1. 是否把构建依赖(Maven、JDK)与运行依赖(JRE)彻底隔离
  2. 是否把源码、缓存、构建产物留在构建阶段,运行阶段只保留最小可执行制品
  3. 是否兼顾国内网络加速(Maven 镜像源、apt/apk 源)、安全加固(非 root、最小镜像、无 shell)、镜像大小(<100 MB 为佳)以及CI/CD 友好性(可参数化、可复用缓存层)。
    答不到这些点,会被认为“只背了模板,没做过生产”。

知识点

  1. 多阶段构建(Multi-stage Build):Docker 17.05+ 原生支持,用 AS 语法给阶段命名,最终镜像只保留最后一个 FROM 的内容。
  2. 构建缓存最大化:把 pom.xml 提前复制并单独 RUN mvn dependency:go-offline,让依赖下载层与源码层解耦,避免每次改一行代码就全量重新下载。
  3. 国内加速
    • Maven:settings.xml 里配置 <mirror> 指向阿里云或腾讯云;
    • 系统包:Debian 用 sed 替换 /etc/apt/sources.list,Alpine 替换 /etc/apk/repositories
  4. 最小运行时
    • 优先用 eclipse-temurin:17-jre-alpine(官方维护、体积 60 MB 左右),而非 openjdk:17-jdk-slim(>200 MB)。
    • addgroup -g 1000 app && adduser -u 1000 -G app -s /bin/false -D app 创建非 root 用户,防止容器逃逸。
  5. 安全与可观测
    • COPY --from=builder 时加 --chown=app:app 避免运行时 chown 再建一层;
    • 暴露端口、健康检查、ENTRYPOINTexec 格式,确保 PID 1 可接收信号。

答案

以下示例基于国内阿里云 Maven 镜像,构建阶段使用 eclipse-temurin:17-jdk,运行阶段使用 eclipse-temurin:17-jre-alpine,最终镜像约 85 MB非 root 启动构建缓存可复用

# =============== 阶段 1:构建阶段 ===============
FROM eclipse-temurin:17-jdk AS builder

# 1. 国内加速:替换 apt 源并安装 Maven(Ubuntu 基础镜像)
RUN sed -i 's|http://archive.ubuntu.com|https://mirrors.aliyun.com|g' /etc/apt/sources.list && \
    apt-get update && \
    apt-get install -y --no-install-recommends maven=3.8.* && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

# 2. 把 settings.xml 提前拷贝,指向阿里云仓库,加速依赖下载
COPY settings.xml /usr/share/maven/ref/

# 3. 仅拷贝 pom.xml,先下载依赖,最大化缓存
WORKDIR /build
COPY pom.xml .
RUN mvn -s /usr/share/maven/ref/settings.xml dependency:go-offline -B

# 4. 再拷贝源码,执行编译 & 打包
COPY src ./src
RUN mvn -s /usr/share/maven/ref/settings.xml clean package -DskipTests

# =============== 阶段 2:运行阶段 ===============
FROM eclipse-temurin:17-jre-alpine

# 1. 安装 gcompat(Alpine 运行某些 JNI 需要)并创建非 root 用户
RUN apk add --no-cache gcompat && \
    addgroup -g 1000 app && \
    adduser  -u 1000 -G app -s /bin/false -D app

WORKDIR /app

# 2. 从构建阶段拷贝 jar,并改属主
COPY --from=builder --chown=app:app /build/target/*.jar app.jar

# 3. 安全加固:只读根、无 shell、非 root
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
  CMD wget -q -O /dev/null http://localhost:8080/actuator/health || exit 1

# 4. 使用 exec 格式,确保 PID 1 可接收 SIGTERM
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

阶段职责说明:
阶段 1(builder):提供完整 JDK + Maven 环境,负责下载依赖、编译源码、生成可执行 jar,产物留在镜像层中,但不会进入最终镜像,因此构建工具与源码不会增加运行时体积。
阶段 2(runtime):仅基于 JRE Alpine 最小镜像,只拷贝构建产物 jar,并做用户降权、健康检查、端口暴露等生产级加固,最终镜像无构建依赖、无源码、无 shell、体积最小,满足“一次构建、随处运行”的容器化交付要求。

拓展思考

  1. CI/CD 流水线缓存:在 GitLab CI 或 GitHub Actions 里,把 /root/.m2/repository 挂载为流水线缓存卷,可让跨次构建依旧享受 dependency:go-offline 层缓存,构建时间从 5 min 降到 30 s
  2. 镜像再瘦身:若项目使用 Spring Boot 3+ Native,可把第二阶段换成 scratchdistroless体积压到 30 MB 以下,但需静态编译并处理 DNS、时区、CA 证书等细节。
  3. 混合架构:国内云厂商已大量上线 ARM 节点,在 docker buildx 里加 --platform linux/amd64,linux/arm64 一次性构建双架构镜像,避免 x86 镜像在鲲鹏或 Graviton 上性能折损。
  4. 安全左移:在构建阶段引入 mvn dependency-check:check 生成 SBOM,配合 trivy image 扫描,阻断含 CVE 的依赖进入最终镜像,实现 DevSecOps。