给出一个 Java Maven 项目的两阶段 Dockerfile 并说明每阶段职责
解读
国内面试官问“两阶段 Dockerfile”时,真正想考察的是你对“构建阶段与运行阶段彻底分离”这一最佳实践的理解深度。
他们不仅希望看到“能跑起来”的镜像,更在意:
- 是否把构建依赖(Maven、JDK)与运行依赖(JRE)彻底隔离;
- 是否把源码、缓存、构建产物留在构建阶段,运行阶段只保留最小可执行制品;
- 是否兼顾国内网络加速(Maven 镜像源、apt/apk 源)、安全加固(非 root、最小镜像、无 shell)、镜像大小(<100 MB 为佳)以及CI/CD 友好性(可参数化、可复用缓存层)。
答不到这些点,会被认为“只背了模板,没做过生产”。
知识点
- 多阶段构建(Multi-stage Build):Docker 17.05+ 原生支持,用
AS语法给阶段命名,最终镜像只保留最后一个FROM的内容。 - 构建缓存最大化:把
pom.xml提前复制并单独RUN mvn dependency:go-offline,让依赖下载层与源码层解耦,避免每次改一行代码就全量重新下载。 - 国内加速:
- Maven:
settings.xml里配置<mirror>指向阿里云或腾讯云; - 系统包:Debian 用
sed替换/etc/apt/sources.list,Alpine 替换/etc/apk/repositories。
- Maven:
- 最小运行时:
- 优先用
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 用户,防止容器逃逸。
- 优先用
- 安全与可观测:
COPY --from=builder时加--chown=app:app避免运行时chown再建一层;- 暴露端口、健康检查、
ENTRYPOINT用exec格式,确保 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、体积最小,满足“一次构建、随处运行”的容器化交付要求。
拓展思考
- CI/CD 流水线缓存:在 GitLab CI 或 GitHub Actions 里,把
/root/.m2/repository挂载为流水线缓存卷,可让跨次构建依旧享受dependency:go-offline层缓存,构建时间从 5 min 降到 30 s。 - 镜像再瘦身:若项目使用 Spring Boot 3+ Native,可把第二阶段换成
scratch或distroless,体积压到 30 MB 以下,但需静态编译并处理 DNS、时区、CA 证书等细节。 - 混合架构:国内云厂商已大量上线 ARM 节点,在
docker buildx里加--platform linux/amd64,linux/arm64一次性构建双架构镜像,避免 x86 镜像在鲲鹏或 Graviton 上性能折损。 - 安全左移:在构建阶段引入
mvn dependency-check:check生成 SBOM,配合trivy image扫描,阻断含 CVE 的依赖进入最终镜像,实现 DevSecOps。