wuee
Java 后端开发 / Docker / 2026.08

Spring Boot 应用 Docker 镜像最佳实践

Spring Boot 3.x · Java 21 · 多阶段构建

把一个 Spring Boot 应用打成 Docker 镜像很简单,FROM eclipse-temurin:21-jreCOPY *.jar app.jar 就完了。但生产环境的镜像要小、要安全、要能被健康检查、要能在容器内存限制下正确工作。本文这套方案经过生产验证,逐条说明为什么这么做。

一、多阶段构建:构建产物不进运行镜像

构建用完整的 Maven + JDK 镜像,运行只用精简的 JRE 镜像,两个阶段互不干扰。最终镜像里只有 jar 和 JRE,没有源码、没有 .m2 缓存。

# ===== 阶段一:构建 =====
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /build

# 先只 COPY pom.xml,利用 Docker 层缓存
# 依赖没变时,这一层不重建,秒级复用
COPY pom.xml .
RUN mvn dependency:go-offline -B

COPY src ./src
RUN mvn package -DskipTests -B

# ===== 阶段二:运行 =====
FROM eclipse-temurin:21-jre
WORKDIR /app

COPY --from=build /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

这里有个容易被忽略的细节:RUN mvn dependency:go-offline 在 COPY pom.xml 之后、COPY src 之前执行。这样只要依赖声明没变,Docker 构建就会复用这一层缓存,不会每次重新下载依赖——大项目的构建时间能差出好几分钟。

二、分层镜像:只更新变化的那一层

Spring Boot 3.x 的 maven 插件默认把 jar 拆成四层:依赖层、Spring Boot 层、应用层、资源层。开启分层后,把 jar 解包到镜像里,推送到镜像仓库时只有变化层需要传输。

<!-- pom.xml:开启分层 -->
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <layers><enabled>true</enabled></layers>
  </configuration>
</plugin>
# Dockerfile 运行阶段配合解包
FROM eclipse-temurin:21-jre
WORKDIR /app

COPY --from=build /build/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]

效果:只改一行业务代码,重建镜像时依赖层、Spring Boot 层全部命中缓存,构建和推送都只动应用层,CI 速度提升明显。

三、非 root 用户运行

默认情况下容器里跑的是 root。一旦应用被攻破,攻击者拿到的是容器内最高权限,配合挂载的目录可能直接波及宿主机。生产镜像必须降权运行。

# Debian 系基础镜像(eclipse-temurin)
RUN groupadd -r app -g 1001 && useradd -r -u 1001 -g app -m -d /app app
USER app

# Alpine 系基础镜像则用
# RUN addgroup -S app && adduser -S -G app app
# USER app
注意:切了非 root 用户后,应用写文件的目录必须提前授权,否则启动时报 Permission denied。比如日志目录:RUN mkdir -p /app/logs && chown -R app:app /app。这也是为什么数据卷的权限问题会追着容器跑——参考网络与数据卷管理一文里的权限处理方案。

四、健康检查:让编排系统知道应用活没活

Spring Boot Actuator 暴露 /actuator/health,Docker 原生 HEALTHCHECK 定期探活。容器被标记为 unhealthy 后,Docker Compose 的 depends_on: condition: service_healthy 才能生效——依赖方不会在数据库还没就绪时启动。

# application.yml 只暴露 health,不暴露其他端点
management:
  endpoints:
    web:
      exposure:
        include: health
  endpoint:
    health:
      show-details: never
# Dockerfile 追加
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD wget -q -O - http://127.0.0.1:8080/actuator/health || exit 1
# docker-compose.yml 中依赖关系
services:
  app:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8080/actuator/health"]
      interval: 30s
      timeout: 3s
      start_period: 40s
      retries: 3
    restart: always

  nginx:
    image: nginx:alpine
    depends_on:
      app:
        condition: service_healthy
start-period 很关键:Spring Boot 启动通常要 10~40 秒(取决于依赖和数据量)。start-period 内的失败不计入重试,避免应用启动稍慢就被误判 unhealthy 反复重启。

五、JVM 感知容器内存

默认 JVM 的堆大小按物理机内存的 1/4 计算。容器里如果限制了 512MB,JVM 却按宿主机 16GB 申请 4GB 堆,直接 OOM 被内核杀掉。用百分比参数让 JVM 跟着容器的 cgroup 限额走:

ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-Xss512k", "-jar", "app.jar"]

配合 Compose 的 limits 一起用:

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

六、时区与编码

容器默认 UTC 时区,Java 应用的时间戳、日志都会差八小时。运行时统一注入,不要每个 Dockerfile 各写一套:

# docker-compose.yml
services:
  app:
    environment:
      - TZ=Asia/Shanghai
      - JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai

七、验证构建结果

# 构建
docker build -t myapp:latest .

# 查看镜像体积(分层构建 + JRE 应该 200~300MB)
docker images myapp

# 本地试运行 + 健康检查
docker run --rm -p 8080:8080 --memory 512m myapp:latest
curl http://127.0.0.1:8080/actuator/health
# → {"status":"UP"}
总结:多阶段构建管体积,分层缓存管速度,非 root 管安全,健康检查管可用性,MaxRAMPercentage 管内存。这套 Dockerfile 模板可以直接复制进新项目。

延伸阅读:Spring Boot 3.x 多环境配置管理实战 · Docker Compose 网络与数据卷管理
← 返回首页