Spring Boot 容器化避坑指南

📅 2026/8/22 3:14:57
Spring Boot 容器化避坑指南
周一早上负责上线的同事在群里发了条消息「镜像 800MB拉取花了 3 分钟测试环境全阻塞了。」底下没人接话。因为所有人都知道问题出在哪Spring Boot 应用被打进 Docker 镜像时大多数人只是把 JDK 和 fat jar 粗暴地塞在一起。这篇文章不讲 Docker 入门只讲 Spring Boot 容器化部署里真正影响生产的东西镜像体积、构建速度、JVM 内存、优雅停机、健康检查。每一条都是线上踩出来的。一、先看大多数人怎么做的FROM openjdk:8-jdk COPY app.jar /app.jar ENTRYPOINT [java, -jar, /app.jar]三行能跑。问题也都在openjdk:8-jdk 基础镜像自带完整 JDK含编译器、调试器体积 500MB 起步fat jar 里塞着所有依赖每次改一行代码整个 jar 重新打包构建缓存全部失效JVM 默认按宿主机内存比例分配堆容器内存限制形同虚设OOM 是常态容器停止时Spring Boot 来不及优雅收尾消息丢失、连接池残留三行代码五个坑。二、多阶段构建把构建环境和运行环境分开核心思路构建阶段用完整的 Maven JDK 镜像完成编译打包运行阶段只保留 JRE 和产物。FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn package -DskipTests -B FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuild /build/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]一个 Dockerfile 两段构建环境里的 Maven、源码、编译中间产物都不会进入最终镜像。基础镜像从 openjdk:8-jdk约 500MB换成 eclipse-temurin:17-jre约 200MB体积直接砍掉一大半。这里还有个关键细节RUN mvn dependency:go-offline单独执行。它把依赖下载和源码编译拆成两层只要 pom.xml 没变Docker 构建缓存就不会失效二次构建从几分钟降到十几秒。三、分层镜像让缓存命中率翻倍Spring Boot 2.3 之后官方插件支持分层打包。jar 里的依赖、Spring Boot 框架层、应用代码层被拆开层与层之间相互独立。spring:boot:build-image:{}或者更直接的方式在 pom.xml 里显式开启plugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactIdconfigurationlayersenabledtrue/enabled/layers/configuration/plugin打包后用java -Djarmodetools -jar app.jar extract解出分层目录Dockerfile 按层 COPYFROM eclipse-temurin:17-jre AS builder WORKDIR /extract COPY --frombuild /build/target/app.jar app.jar RUN java -Djarmodetools -jar app.jar extract --layers --destination extracted FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /extract/extracted/dependencies/ ./ COPY --frombuilder /extract/extracted/spring-boot-loader/ ./ COPY --frombuilder /extract/extracted/snapshot-dependencies/ ./ COPY --frombuilder /extract/extracted/application/ ./ ENTRYPOINT [java, org.springframework.boot.loader.launch.JarLauncher]依赖层和快照依赖层在最前面应用代码层在最后。改业务代码只重建最后一层拉镜像时其余层全部命中本地缓存。团队人多、发版频繁时这个收益非常明显。四、JVM 必须感知容器内存这是容器化最容易翻车的一环。JDK 8u191 之前的版本JVM 不认识 cgroup 限制默认按宿主机物理内存的 1/4 分配堆。宿主机 64GB容器限制 512MBJVM 一启动就想吃 16GB容器直接被 OOM Killer 干掉。现代 JDK8u191 / 11默认开启-XX:UseContainerSupport会读取 cgroup 配额。但只认配额还不够推荐显式指定比例参数ENV JAVA_OPTS-XX:MaxRAMPercentage75.0 -XX:InitialRAMPercentage50.0 -XX:MinRAMPercentage50.0 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]MaxRAMPercentage75的意思是堆最大占容器内存的 75%给 Metaspace、线程栈、Direct Memory 留出余量。这个参数必须写在 ENTRYPOINT 里通过环境变量注入这样运维不用改镜像也能调内存k8s 里按 Deployment 的 resources.limits 调整即可。不要再用-Xmx2g这种硬编码。容器规格一变镜像就得重发。五、时区、用户、编码三个小坑一次填平生产环境最常见的三连问日志时间为什么差 8 小时容器里为什么是 root中文为什么乱码ENV TZAsia/Shanghai \ LANGC.UTF-8 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN useradd -r -s /sbin/nologin app USER appTZ 环境变量解决容器默认 UTC 导致的日志时间偏移非 root 运行是安全基线容器逃逸后 root 权限等于宿主权限LANG 解决 JVM 读文件时的编码推断问题六、优雅停机别让流量断在重启时Docker stop 默认发 SIGTERM15 秒后强制 SIGKILL。Spring Boot 2.3 的优雅停机需要显式开启server:shutdown:gracefulspring:lifecycle:timeout-per-shutdown-phase:30s开启后收到 SIGTERM 时 Spring Boot 会停止接收新请求等待在处理请求完成再销毁 Bean、释放连接。配合 k8s 的 preStop hook 和 Readiness 探针滚动发布可以实现零中断。注意 timeout-per-shutdown-phase 要小于 Docker 的 stop-timeout 和 k8s 的 terminationGracePeriodSeconds否则请求还没处理完就被 SIGKILL 了。七、健康检查让编排系统知道应用活着Dockerfile 加一行Docker/K8s 才能判断容器是否真的可用HEALTHCHECK --interval30s --timeout5s --start-period60s --retries3 \ CMD curl -fs http://localhost:8080/actuator/health || exit 1配合 Actuator 的 health 端点除了进程存活还能覆盖数据库连接、Redis、消息队列的可用性。基础镜像里没有 curl 时用 wget 或 Java 的 HttpURLConnection 替代。八、完整示例一份可以直接上生产的 DockerfileFROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn package -DskipTests -B FROM eclipse-temurin:17-jre ENV TZAsia/Shanghai \ LANGC.UTF-8 \ JAVA_OPTS-XX:MaxRAMPercentage75.0 -XX:InitialRAMPercentage50.0 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ useradd -r -s /sbin/nologin app WORKDIR /app COPY --frombuild /build/target/app.jar app.jar USER app EXPOSE 8080 HEALTHCHECK --interval30s --timeout5s --start-period60s --retries3 \ CMD wget -q -O- http://localhost:8080/actuator/health || exit 1 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]构建dockerbuild-tdemo-app:1.0.0.dockerrun-d-p8080:8080--memory1g--cpus1demo-app:1.0.0九、最后对一下账这套方案做完效果是可量化的镜像体积约 500MB 降到约 200MB拉取时间减少一半以上二次构建依赖层缓存命中后从几分钟降到十几秒内存JVM 按容器配额分配不再和宿主机抢资源停机优雅停机生效滚动发布不再出现 5xx安全非 root 运行健康检查接入编排系统Spring Boot 容器化从来不是「写个 Dockerfile 就能跑」的事。镜像体积、构建缓存、内存感知、优雅停机每一环都在生产环境等着你。你现在的镜像多大构建一次要多久