Spring Boot 部署运维
本文范围
这篇文章只讲 Spring Boot 后端服务从 jar 到生产入口的部署运维链路:构建可执行 jar、profiles、外部配置、环境变量、日志目录、JVM 参数、nohup、systemd、端口与防火墙、Actuator 健康检查、优雅停机、Docker 化、Nginx 代理、发布验证、回滚和生产排障。
本文不展开 Spring MVC、Spring Data、事务、缓存、微服务治理、业务代码设计和容器编排平台完整教程。后面涉及 Docker、Nginx、K8S、Maven,只从“Spring Boot 服务怎么稳定上线、怎么被托管、怎么排障”的部署运维角度讲。
读完以后,你应该能把 java -jar 变成一套可审计的生产流程:配置不混进 jar、日志能保留现场、健康检查真实有效、故障能抓 JVM 证据、发布失败能回滚。
前置条件
建议先准备:
- 一个已经通过测试的 Spring Boot 项目,能用 Maven 或 Gradle 打出可执行 jar。
- 一台安装 JDK 的 Linux 服务器;截至 2026-07-10,Spring Boot 4.1.0 官方要求至少 Java 17,并兼容到 Java 26。老项目使用 Spring Boot 2.x / Java 8 时,要回到对应版本文档校准参数。
- 一个普通运行用户,例如
appuser,不要用 root 直接跑业务进程。 - 明确的部署目录、配置目录、日志目录、备份目录和回滚目录。
- 具备
java、jar、curl、ss、systemctl、journalctl、jcmd、jstack、jstat这些基础验证和排障工具。
很多 Spring Boot 服务第一次上线,都是从这一句开始的:
java -jar app.jar命令本身没问题,问题是生产环境不会只问“能不能跑起来”。生产环境还会继续问:
用的是哪个 profile?
配置文件从哪里加载?
日志写到哪里?
端口被谁占了?
OOM 后有没有 dump?
systemd 能不能托管?
Nginx 502 怎么查?
Docker 容器重启后数据还在不在?
发布失败能不能回滚?
内网断网还能不能构建?这篇就按真实上线顺序来走。先把 jar 跑起来,再一点点补齐配置、日志、健康检查、进程管理、Docker、Nginx、发布验证和故障排查。
这里先说明一下版本约束。写作时 Spring Boot 官方文档中,Spring Boot 4.1.0 要求至少 Java 17,并兼容到 Java 26。生产上如果还在用 Spring Boot 2.x 或 Java 8,命令思路一样,但 JVM 日志参数、插件版本和部分 Actuator 行为要回到对应版本文档里确认。
Spring Boot 生产运行拓扑图
Spring Boot 部署不是把 java -jar 放进后台就结束。生产运行时,服务会同时处在入口流量、进程托管、外部配置、日志现场、健康检查、依赖组件和监控告警这些关系里。先看这张拓扑,后面的每个章节都是在补其中一个节点。
这张图决定了排查顺序:入口 502 不一定是应用崩了,可能是 Nginx 上游、DNS、证书或防火墙;Actuator UP 也不代表业务链路都通,因为数据库、MQ、第三方接口和入口层还要单独验证;日志里没有错误,也可能是日志目录、权限或采集链路出了问题。
最小验证命令可以沿着拓扑跑:
readlink -f /opt/apps/order-service/current
ps -ef | grep '[j]ava'
ss -lntp | grep ':8080'
curl -fsS http://127.0.0.1:8080/actuator/health
journalctl -u order-service -n 200 --no-pager
tail -n 100 /opt/apps/order-service/logs/app.log如果要确认 systemd 注入的环境变量,不要只看当前 shell:
PID=$(pgrep -f 'order-service.*app.jar' | head -n 1)
tr '\0' '\n' < /proc/${PID}/environ | grep -E 'SPRING_|JAVA_|SERVER_'常见坑是把 Spring Boot 当成单进程工具来部署:本机 curl 正常,却没验证 Nginx;Actuator 暴露太多端点,被公网访问;容器设置了内存限制,但 JVM 参数还按宿主机内存估;配置中心覆盖了本地配置,现场只查 jar 包旁边的 application.yml。生产约束是:健康检查端点要限制暴露范围;日志和 dump 目录要有容量上限;运行用户不要用 root;发布包和镜像必须有可追溯指纹。
一、先确认 jar 包是不是能启动
上线前第一件事,不是急着写 systemd,也不是急着做 Docker 镜像,而是先确认这个 jar 包到底是不是 Spring Boot 可执行 jar。
一般项目里会有这个 Maven 插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>有了它,Maven 打包时会把普通 jar 重新打成 Spring Boot 可执行 jar。
执行构建:
mvn -B clean verify这里 -B 表示 batch 模式,适合 CI/CD,不会在终端里等你交互输入。clean verify 会清理旧产物并跑完整验证流程。
如果测试已经在流水线前面跑过,临时打包可以这样:
mvn -B clean package -DskipTests命令执行完,先看 target 目录:
ls -lh target/*.jar你应该能看到类似这样的文件:
app.jar
app.jar.original这里要注意,真正拿去部署的一般是重新打包后的 app.jar,不是 .original 文件。.original 通常是 Maven 原始产物,不包含 Spring Boot 启动加载器。
再看 jar 里面的结构:
jar tf target/app.jar | head -n 30正常的 Spring Boot 可执行 jar 里一般能看到:
META-INF/
BOOT-INF/classes/
BOOT-INF/lib/
org/springframework/boot/loader/BOOT-INF/classes 放的是你的业务 class,BOOT-INF/lib 放的是依赖 jar。也就是说,这个 jar 已经把运行时依赖打进去了,所以才能直接 java -jar 启动。
继续看启动入口:
unzip -p target/app.jar META-INF/MANIFEST.MF你应该能看到类似内容:
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.Application不同 Spring Boot 大版本里 loader 类名可能略有不同,不要死背类名。你只要记住两件事:Main-Class 是 Spring Boot loader,Start-Class 是你的业务启动类。
发布前建议顺手记录校验值:
sha256sum target/app.jar以后排查“线上到底跑的是不是这次发布的包”,这个值很有用。
如果这里失败,先不要往后走。常见原因一般是:
没有配置 spring-boot-maven-plugin
打包命令执行失败
JDK 版本和项目编译版本不匹配
拿错了普通 jar 或 original jar二、第一次手工启动
jar 包确认没问题后,先在服务器上手工启动一次。这个阶段的目的不是长期运行,而是确认 JDK、端口、配置和依赖能不能通。
最小启动命令:
java -jar app.jar访问健康接口之前,先看进程有没有起来:
ps -ef | grep '[j]ava'如果能看到 Java 进程,说明进程至少启动了。然后看端口:
ss -lntp | grep ':8080'如果没有 ss,可以用:
netstat -lntp | grep ':8080'这里要注意,端口号不一定是 8080,要以你的 server.port 为准。
指定端口启动:
java -jar app.jar --server.port=8080指定生产环境启动:
java -jar app.jar --spring.profiles.active=prod如果还要加 JVM 参数,参数位置不要放错:
java -Xms512m -Xmx512m -jar app.jar --spring.profiles.active=prod --server.port=8080规则很简单:
JVM 参数放在 -jar 前面
Spring Boot 应用参数放在 jar 包后面比如:
java -Xms512m -Xmx512m -jar app.jar这里的 -Xms512m -Xmx512m 是 JVM 参数。
java -jar app.jar --server.port=8080 --spring.profiles.active=prod这里的 --server.port 和 --spring.profiles.active 是应用参数。
如果启动失败,第一眼先看终端输出。常见错误一般长这样:
no main manifest attribute
UnsupportedClassVersionError
Port already in use
Failed to configure a DataSource
Connection refused对应处理思路:
no main manifest attribute:jar 不是可执行 jar,回去检查打包插件
UnsupportedClassVersionError:运行服务器 JDK 版本太低
Port already in use:端口被占用
Failed to configure a DataSource:数据库配置没加载或配置不完整
Connection refused:依赖服务地址不通、端口不通或账号密码错误查 JDK:
java -version查端口占用:
lsof -i :8080本机访问:
curl -v http://127.0.0.1:8080/访问后如果看到业务响应、登录页、404 或 Spring 默认错误页,都说明 HTTP 服务已经接到了请求。404 不一定是启动失败,它可能只是你访问的路径不存在。
三、profiles 和配置文件不要混在 jar 里
生产部署最容易出事故的地方,就是配置。很多服务在测试环境好好的,上生产就连错库、读错 Redis、日志写不到目录,最后一查是 profile 没生效。
常见配置文件是这样放的:
application.yml
application-dev.yml
application-test.yml
application-prod.ymlapplication.yml 放公共配置:
spring:
application:
name: order-service
server:
port: 8080application-prod.yml 放生产差异配置:
spring:
datasource:
url: jdbc:mysql://db.example.com:3306/order_db
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
logging:
file:
name: ${LOG_DIR:/opt/apps/order-service/logs}/app.log这里要注意,生产密码不要写死在文件里,更不要提交到 Git。上面这种 ${DB_PASSWORD} 的写法,就是让密码从环境变量里进来。
启动时指定 profile:
java -jar app.jar --spring.profiles.active=prod也可以用环境变量:
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar一般生产更喜欢环境变量或部署平台变量,因为不用改启动命令。
再准备一个应用目录:
/opt/apps/order-service/
current/
app.jar
config/
application-prod.yml
logs/
tmp/如果目录没有就新建:
sudo mkdir -p /opt/apps/order-service/{current,config,logs,tmp}把外部配置放到 config 目录后,启动时告诉 Spring Boot 额外加载这个目录:
java -jar /opt/apps/order-service/current/app.jar \
--spring.profiles.active=prod \
--spring.config.additional-location=file:/opt/apps/order-service/config/这里用的是 additional-location,意思是在默认配置之外,再额外加载这个目录。一般生产比 spring.config.location 更稳一点,因为不容易把默认加载逻辑整个覆盖掉。
如果你就是想完全指定配置文件,可以这样:
java -jar app.jar \
--spring.config.location=file:/opt/apps/order-service/config/application-prod.yml配置改完以后,怎么看有没有生效?
先看启动日志里激活了哪个 profile:
grep -i "profile" /opt/apps/order-service/logs/app.log再看进程实际启动参数:
ps -ef | grep '[j]ava'如果你用了环境变量,查进程环境:
PID="$(pgrep -f 'order-service.*app.jar')"
tr '\0' '\n' < /proc/${PID}/environ | sort | grep -E 'SPRING|SERVER|LOG|DB|REDIS'如果启用了 Actuator,并且只在受控内网开放,可以临时看环境端点:
curl -s http://127.0.0.1:8080/actuator/env | head这里要注意,/actuator/env 可能暴露敏感配置,生产不要对外开放。
如果部署平台适合注入一段 JSON,也可以用 SPRING_APPLICATION_JSON:
export SPRING_APPLICATION_JSON='{"server":{"port":8080},"spring":{"profiles":{"active":"prod"}}}'
java -jar app.jar这个适合少量结构化参数。配置一多,还是外部 YAML 或配置中心更清楚。
配置未生效时,按这个顺序查:
第一步:ps 看进程参数
第二步:日志看 active profiles
第三步:查进程环境变量
第四步:确认外部配置文件路径和权限
第五步:如果有配置中心,再看配置中心是否覆盖了本地配置最常见的坑是:你在当前 shell 里 export 了变量,但服务是 systemd 启动的。systemd 不会自动继承你当前终端里的变量,必须写到 EnvironmentFile 里。
四、日志目录先规划好
日志不要等出事了再找。上线前就要明确三类日志:
应用日志:业务异常、请求链路、错误栈
启动日志:nohup 或 systemd 捕获的标准输出
GC 日志:排查内存和停顿Spring Boot 最小日志配置:
logging:
file:
name: ${LOG_DIR:./logs}/app.log
level:
root: INFO
com.example: INFO如果你使用 Logback,可以做滚动配置:
<configuration>
<property name="LOG_DIR" value="${LOG_DIR:-./logs}"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>创建日志目录:
sudo mkdir -p /opt/apps/order-service/logs
sudo chown -R appuser:appuser /opt/apps/order-service/logs启动后查看日志:
tail -n 200 -f /opt/apps/order-service/logs/app.log如果没有日志,先不要猜代码没执行,按下面查:
ls -ld /opt/apps/order-service/logs
ls -lh /opt/apps/order-service/logs看 systemd 日志:
journalctl -u order-service -n 200 --no-pager看最近有没有错误:
grep -iE "exception|error|failed|timeout|oom|refused" /opt/apps/order-service/logs/app.log | tail -n 100一般日志看不到就这几类原因:
日志目录不存在
运行用户没有写权限
LOG_DIR 环境变量没传进去
Logback 配置没有加载
服务还没启动到写应用日志那一步
Docker 里只输出 stdout,没有写文件生产建议是:单机场景保留文件日志,容器场景优先 stdout 加日志采集,但核心错误日志和 GC 日志仍要有明确位置。
五、JVM 参数要带着故障现场
很多人只配 -Xms 和 -Xmx,真正 OOM 的时候才发现没有 heap dump,没有 GC 日志,现场已经没了。
一个比较实用的 Java 17 参数示例:
JAVA_OPTS="
-Xms512m
-Xmx512m
-Dfile.encoding=UTF-8
-Duser.timezone=Asia/Shanghai
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/order-service/logs
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/opt/apps/order-service/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m
"这里逐个说一下:
-Xms512m:初始堆大小
-Xmx512m:最大堆大小
-Dfile.encoding=UTF-8:避免编码问题
-Duser.timezone=Asia/Shanghai:避免日志时间和业务时间对不上
-XX:+HeapDumpOnOutOfMemoryError:OOM 时自动 dump
-XX:HeapDumpPath:dump 文件保存目录
-XX:+ExitOnOutOfMemoryError:OOM 后退出,让托管系统拉起新进程
-Xlog:gc*:记录 GC 日志启动后看 GC 日志:
tail -n 100 /opt/apps/order-service/logs/gc.log如果文件没有生成,先确认 JVM 参数有没有真正带进去:
ps -ef | grep '[j]ava'Java 8 的 GC 日志参数不是 -Xlog:gc*,而是老参数体系,比如 -Xloggc、-XX:+PrintGCDetails。所以老项目不要直接复制 Java 17 的参数。
容器里还要注意内存限制。如果 Docker 给容器限制了 1G 内存,不要直接写 -Xmx1g,因为 JVM 除了堆,还有 Metaspace、线程栈、直接内存、JIT 和 native 内存。
容器里可以用比例参数:
JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs"启动时看容器内存:
docker stats order-service如果看到容器频繁重启,再查是否被 OOM Kill:
docker inspect order-service --format='OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'六、用 nohup 跑起来
nohup 适合测试机、小型环境或临时验证。生产更推荐 systemd 或容器编排,但你一定会遇到只有一台 Linux 机器、先把服务跑起来的场景。
先手工跑一次:
cd /opt/apps/order-service/current
nohup java $JAVA_OPTS -jar app.jar $APP_OPTS \
> /opt/apps/order-service/logs/startup.log 2>&1 &
echo $! > /opt/apps/order-service/order-service.pid这里做了三件事:
nohup:终端关闭后进程继续运行
startup.log:保存标准输出和标准错误
pid 文件:后面停止进程时能找到它查看:
cat /opt/apps/order-service/order-service.pid
ps -fp "$(cat /opt/apps/order-service/order-service.pid)"
tail -n 100 -f /opt/apps/order-service/logs/startup.log一般需要把这些命令收进脚本。
start.sh:
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="order-service"
APP_HOME="/opt/apps/${APP_NAME}"
JAR_FILE="${APP_HOME}/current/app.jar"
PID_FILE="${APP_HOME}/${APP_NAME}.pid"
LOG_DIR="${APP_HOME}/logs"
mkdir -p "${LOG_DIR}"
if [ -f "${PID_FILE}" ] && kill -0 "$(cat "${PID_FILE}")" 2>/dev/null; then
echo "${APP_NAME} is already running, pid=$(cat "${PID_FILE}")"
exit 0
fi
JAVA_OPTS="${JAVA_OPTS:--Xms512m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai}"
JAVA_OPTS="${JAVA_OPTS} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}"
JAVA_OPTS="${JAVA_OPTS} -XX:+ExitOnOutOfMemoryError"
JAVA_OPTS="${JAVA_OPTS} -Xlog:gc*:file=${LOG_DIR}/gc.log:time,uptime,level,tags:filecount=5,filesize=100m"
APP_OPTS="${APP_OPTS:---spring.profiles.active=prod --spring.config.additional-location=file:${APP_HOME}/config/ --logging.file.name=${LOG_DIR}/app.log}"
nohup java ${JAVA_OPTS} -jar "${JAR_FILE}" ${APP_OPTS} > "${LOG_DIR}/startup.log" 2>&1 &
echo $! > "${PID_FILE}"
echo "${APP_NAME} started, pid=$(cat "${PID_FILE}")"执行:
chmod +x start.sh
./start.sh启动后应该能看到:
order-service started, pid=xxxxx再验证端口和健康检查:
ss -lntp | grep ':8080'
curl -fsS http://127.0.0.1:8080/actuator/healthstop.sh:
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="order-service"
APP_HOME="/opt/apps/${APP_NAME}"
PID_FILE="${APP_HOME}/${APP_NAME}.pid"
TIMEOUT="${STOP_TIMEOUT:-60}"
if [ ! -f "${PID_FILE}" ]; then
echo "pid file not found"
exit 0
fi
PID="$(cat "${PID_FILE}")"
if ! kill -0 "${PID}" 2>/dev/null; then
echo "${APP_NAME} is not running"
rm -f "${PID_FILE}"
exit 0
fi
kill "${PID}"
for i in $(seq 1 "${TIMEOUT}"); do
if ! kill -0 "${PID}" 2>/dev/null; then
rm -f "${PID_FILE}"
echo "${APP_NAME} stopped"
exit 0
fi
sleep 1
done
echo "${APP_NAME} stop timeout, pid=${PID}"
exit 1这里要注意,脚本里没有默认 kill -9。正常停机应该先发 SIGTERM,让 Spring Boot 有机会优雅停机、释放连接、写完日志。只有确认进程卡死时,才人工强杀。
restart.sh:
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
"${SCRIPT_DIR}/stop.sh"
"${SCRIPT_DIR}/start.sh"重启后看结果:
./restart.sh
curl -fsS http://127.0.0.1:8080/actuator/health如果访问后返回:
{"status":"UP"}说明应用健康检查通过。
七、用 systemd 托管服务
nohup 能用,但它不是生产最舒服的方式。生产单机部署一般需要 systemd,因为它能做开机自启、进程托管、失败重启、统一日志。
先创建运行用户:
sudo useradd -r -s /sbin/nologin appuser
sudo chown -R appuser:appuser /opt/apps/order-service如果用户已经存在,这一步会提示存在,确认用户和权限即可。
再准备环境文件:
sudo mkdir -p /etc/sysconfig
sudo vi /etc/sysconfig/order-service写入:
JAVA_OPTS="-Xms512m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/apps/order-service/logs -XX:+ExitOnOutOfMemoryError -Xlog:gc*:file=/opt/apps/order-service/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m"
APP_OPTS="--spring.profiles.active=prod --spring.config.additional-location=file:/opt/apps/order-service/config/ --logging.file.name=/opt/apps/order-service/logs/app.log"这里要注意,systemd 不会自动读取你当前 shell 的 export。想让 systemd 启动时有这些变量,就写到 EnvironmentFile。
创建 service 文件:
sudo vi /etc/systemd/system/order-service.service内容:
[Unit]
Description=Order Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/apps/order-service/current
EnvironmentFile=-/etc/sysconfig/order-service
ExecStart=/usr/bin/java $JAVA_OPTS -jar /opt/apps/order-service/current/app.jar $APP_OPTS
SuccessExitStatus=143
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=60
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target几个地方要看明白:
User=appuser:不要用 root 跑业务服务
WorkingDirectory:避免相对路径跑偏
EnvironmentFile:统一管理 JVM 和应用参数
SuccessExitStatus=143:SIGTERM 停 Java 进程时经常是 143,视为正常退出
Restart=on-failure:异常退出自动拉起
TimeoutStopSec=60:给优雅停机留 60 秒加载 systemd 配置:
sudo systemctl daemon-reload设置开机自启:
sudo systemctl enable order-service启动:
sudo systemctl start order-service看状态:
systemctl status order-service --no-pager正常应该看到 active (running)。
看日志:
journalctl -u order-service -n 200 --no-pager实时跟日志:
journalctl -u order-service -f如果服务刚启动就失败,应用日志里可能什么都没有。这时候第一现场就是 systemd 日志:
journalctl -u order-service -n 300 --no-pager常见问题:
ExecStart 路径写错:提示 unable to access jarfile
Java 路径不对:提示 /usr/bin/java 找不到
配置文件没权限:应用启动失败或读取不到配置
日志目录没权限:应用启动后写不了 app.log
端口占用:启动日志里提示 port already in use八、端口、防火墙和本机验证
服务启动后,不要直接让别人访问。先在服务器本机确认。
查监听端口:
ss -lntp | grep ':8080'结果里如果看到:
LISTEN 0 ... 0.0.0.0:8080 ...说明服务监听了所有网卡。
如果看到:
127.0.0.1:8080说明只能本机访问,外部机器访问不到。这个不一定是问题,如果前面有 Nginx 代理,本机监听反而更安全。
本机访问:
curl -v http://127.0.0.1:8080/actuator/health如果本机能访问,远程不能访问,再查防火墙。
firewalld:
sudo firewall-cmd --list-ports
sudo firewall-cmd --add-port=8080/tcp --permanent
sudo firewall-cmd --reloadufw:
sudo ufw status
sudo ufw allow 8080/tcp生产建议是:应用端口不直接对公网开放,只开放 Nginx、网关或负载均衡端口。应用端口可以只允许本机或内网访问。
九、Actuator 健康检查
没有健康检查的服务,上线后只能靠猜。Spring Boot 推荐用 Actuator。
先加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>生产最小配置:
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: never
probes:
enabled: true启动后访问:
curl -fsS http://127.0.0.1:8080/actuator/health正常应该看到:
{"status":"UP"}如果启用了 probes,还可以访问:
curl -fsS http://127.0.0.1:8080/actuator/health/liveness
curl -fsS http://127.0.0.1:8080/actuator/health/readiness这里要注意,健康检查不是越详细越好。生产对外一般只给 health。env、heapdump、loggers 这些端点不要暴露到公网。
如果想把管理端口和业务端口分开:
management:
server:
port: 18080
endpoints:
web:
exposure:
include: health,info,prometheus然后只允许监控系统访问 18080。
健康检查失败时这样看:
curl -i http://127.0.0.1:8080/actuator/health如果是 503,说明应用启动了,但健康状态不通过。再看日志:
grep -iE "health|datasource|redis|rabbit|kafka|timeout|refused" /opt/apps/order-service/logs/app.log | tail -n 100很多健康失败不是 Spring Boot 自己坏了,而是数据库、Redis、MQ、下游 HTTP 服务不可用。
十、优雅停机
生产重启服务,不要上来就 kill -9。Spring Boot 可以配置优雅停机:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s正常停服务:
systemctl stop order-service或者:
kill <pid>Docker:
docker stop --time 60 order-service这里的重点是发 SIGTERM,让应用有机会处理完正在执行的请求,关闭连接池,刷新日志。
发布时建议按这个顺序:
先摘流量
等待已有请求处理完成
停止旧服务
替换 jar 或镜像
启动新服务
健康检查通过
恢复流量如果没有负载均衡摘流能力,也至少要确认重启窗口业务可以接受。
十一、Docker 化部署
Spring Boot 做 Docker 镜像,第一版不要写得太花。先保证镜像能启动、日志能挂出来、参数能注入、健康能检查。
Dockerfile 示例:
FROM eclipse-temurin:17-jre
WORKDIR /app
RUN groupadd -r app && useradd -r -g app app
COPY target/app.jar /app/app.jar
RUN mkdir -p /app/logs && chown -R app:app /app
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]构建镜像:
docker build -t registry.example.com/order-service:1.0.0 .这里 -t 是给镜像打标签,最后这个点表示 Dockerfile 所在目录。
查看镜像:
docker images | grep order-service启动容器:
docker run -d \
--name order-service \
--restart=unless-stopped \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e SERVER_PORT=8080 \
-e LOG_DIR=/app/logs \
-e JAVA_TOOL_OPTIONS="-Xms512m -Xmx512m -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Shanghai -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs -XX:+ExitOnOutOfMemoryError -Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m" \
-v /data/logs/order-service:/app/logs \
registry.example.com/order-service:1.0.0这条命令比较长,逐个看:
-d:后台运行
--name:指定容器名,后面查日志、停止容器都用它
--restart=unless-stopped:异常退出后自动重启
-p 8080:8080:宿主机端口映射到容器端口
-e:注入环境变量
JAVA_TOOL_OPTIONS:JVM 会自动读取的参数
-v:把宿主机日志目录挂到容器里启动后查看:
docker ps
docker logs --tail=200 -f order-service
curl -fsS http://127.0.0.1:8080/actuator/health访问后应该看到:
{"status":"UP"}这里要特别讲一下容器文件系统。Docker 使用分层镜像,镜像层是只读的,容器启动后会在上面加一个可写层。你在容器里写的日志、临时文件,默认都写到容器可写层。容器删除后,这些数据也就跟着没了。
所以生产里日志、配置、上传文件不要只放容器可写层,要用 volume 或挂载目录:
-v /data/logs/order-service:/app/logs
-v /data/config/order-service:/app/config:ro这背后就是 Overlay2 和 CoW 的实际意义:镜像层共享,容器层只记录变化。这样启动快、占用少,但你不能把容器层当持久化磁盘用。
查看容器资源:
docker stats order-service限制内存:
docker run -d \
--name order-service \
--memory=1g \
-e JAVA_TOOL_OPTIONS="-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=70 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs" \
registry.example.com/order-service:1.0.0如果容器一直重启:
docker ps -a | grep order-service
docker logs --tail=300 order-service
docker inspect order-service --format='OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'一般要看三件事:退出码、日志最后一屏、是否 OOMKilled。
docker compose 示例:
services:
order-service:
image: registry.example.com/order-service:1.0.0
container_name: order-service
restart: unless-stopped
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
SERVER_PORT: 8080
LOG_DIR: /app/logs
JAVA_TOOL_OPTIONS: >-
-Xms512m
-Xmx512m
-Dfile.encoding=UTF-8
-Duser.timezone=Asia/Shanghai
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/app/logs
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m
volumes:
- /data/logs/order-service:/app/logs
- /data/config/order-service:/app/config:ro
command:
- "--spring.config.additional-location=file:/app/config/"部署:
docker compose pull
docker compose up -d
docker compose ps
docker logs --tail=200 -f order-service如果 compose 文件改了环境变量或镜像 tag,一般需要重新 up -d,否则旧容器还在按旧配置跑。
十二、Nginx 代理
Spring Boot 应用端口一般不直接暴露公网,前面放 Nginx 更常见。
配置示例:
upstream order_service {
server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
server_name order.example.com;
access_log /var/log/nginx/order-service.access.log;
error_log /var/log/nginx/order-service.error.log;
location / {
proxy_pass http://order_service;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}配置写完先检查:
nginx -t看到 syntax is ok 和 test is successful 再重载:
nginx -s reload访问:
curl -I http://order.example.com/
curl -fsS http://order.example.com/actuator/health如果 Nginx 返回 502,先不要改应用代码,按顺序查:
tail -n 100 /var/log/nginx/order-service.error.log
curl -v http://127.0.0.1:8080/actuator/health
ss -lntp | grep ':8080'常见判断:
502:后端没启动、端口错、连接被拒绝、容器端口没映射
504:后端处理超时、线程卡死、SQL 慢、GC 长暂停
404:Nginx 路径和应用 context-path 不一致
413:上传文件太大,需要调 client_max_body_size如果应用配置了 context-path:
server:
servlet:
context-path: /api那健康检查路径也要跟着变:
curl -fsS http://127.0.0.1:8080/api/actuator/health十三、发布验证和回滚
上线不是服务启动就结束。你要能证明新版本真的跑起来,也要能在异常时退回去。
发布验证与回滚链路图
发布验证和回滚要按同一条链路设计。验证不是只看 systemctl status,回滚也不是只把软链指回上一版;你要同时确认版本指纹、入口流量、业务冒烟、日志指标和数据兼容边界。
这张图的关键是把“能退回去”变成可验证事实。发布前没有记录指纹,回滚时就只能凭文件名猜;入口验证缺失,服务本机正常也可能用户仍然 502;数据兼容没有预案,旧版本启动成功也可能处理不了新表结构、新消息格式或新缓存 key。
发布和回滚时至少保留这些命令输出:
git rev-parse --short HEAD
sha256sum app.jar
readlink -f /opt/apps/order-service/current
systemctl restart order-service
systemctl status order-service --no-pager
curl -fsS http://127.0.0.1:8080/actuator/health
curl -fsS https://example.com/api/health
tail -n 100 /opt/apps/order-service/logs/app.log常见坑是只回滚 jar,不回滚配置;只换镜像 tag,不确认 Compose 文件、环境变量和挂载目录;只看 health,不跑核心业务冒烟;只回滚应用,不判断数据库、MQ、缓存和配置中心是否已经被新版本写入了不兼容状态。生产约束是:每次发布必须保留上一版制品和配置快照;禁止用不可追溯的 latest 当生产版本;数据库变更优先做向前兼容,真正不可逆的 SQL 不能依赖应用回滚兜底。
推荐目录:
/opt/apps/order-service/
releases/
20260709-150000/
app.jar
20260709-160000/
app.jar
current -> /opt/apps/order-service/releases/20260709-160000
config/
logs/发布新版本:
mkdir -p /opt/apps/order-service/releases/20260709-160000
cp app.jar /opt/apps/order-service/releases/20260709-160000/app.jar
ln -sfn /opt/apps/order-service/releases/20260709-160000 /opt/apps/order-service/current
systemctl restart order-service查看当前版本目录:
readlink -f /opt/apps/order-service/current启动后验证:
systemctl status order-service --no-pager
ss -lntp | grep ':8080'
curl -fsS http://127.0.0.1:8080/actuator/health
tail -n 100 /opt/apps/order-service/logs/app.log如果有版本接口或 Actuator info:
curl -fsS http://127.0.0.1:8080/actuator/info回滚到上一版:
ln -sfn /opt/apps/order-service/releases/20260709-150000 /opt/apps/order-service/current
systemctl restart order-service再验证:
readlink -f /opt/apps/order-service/current
curl -fsS http://127.0.0.1:8080/actuator/healthDocker 回滚也是同一个思路,镜像 tag 换回上一版:
docker compose down
# 修改 image tag 为上一版本
docker compose up -d
docker compose ps
docker logs --tail=100 order-service这里要注意,不是所有版本都能无脑回滚。如果这次发布改了数据库表结构、消息格式、缓存 key 或外部接口协议,回滚前要确认旧代码还能识别新数据。
比较稳的发布策略是:
先加字段,不马上删字段
先兼容新旧消息格式,再切流量
先让新代码兼容旧配置,再清理旧配置十四、中国大陆、内网和离线构建
很多内网环境里,服务不是死在启动命令上,而是死在“构建机器拉不到依赖”“Docker 拉不到基础镜像”“离线环境没有 Maven 插件”。
企业里一般需要一个内网制品仓库,比如 Nexus、Artifactory 或云厂商制品库。Maven 的 settings.xml 可以这样写:
<settings>
<localRepository>/data/maven/repository</localRepository>
<mirrors>
<mirror>
<id>internal-maven</id>
<mirrorOf>*</mirrorOf>
<url>https://repo.example.com/repository/maven-public/</url>
</mirror>
</mirrors>
<servers>
<server>
<id>internal-maven</id>
<username>${env.MAVEN_REPO_USERNAME}</username>
<password>${env.MAVEN_REPO_PASSWORD}</password>
</server>
</servers>
</settings>这里要注意,示例地址要换成自己的内网仓库地址。真实账号密码不要写进仓库,使用环境变量或 CI/CD 凭据。
使用指定 settings 构建:
mvn -s settings.xml -B clean verify上线前预拉依赖:
mvn -s settings.xml -B dependency:go-offline真正验证离线能力,要断网或使用离线模式跑一次:
mvn -s settings.xml -o -B clean verify如果这里失败,就说明依赖闭环还没做好。常见漏项是 Maven 插件没有缓存,只缓存了业务依赖。
内网构建至少要闭环这些东西:
Maven 依赖
Maven 插件
私有 jar
JDK 安装包或基础镜像
Docker 基础镜像
应用镜像
操作系统 yum / apt 包Docker 镜像离线交付时,在线环境保存:
docker pull registry.example.com/order-service:1.0.0
docker save registry.example.com/order-service:1.0.0 -o order-service-1.0.0.tar
sha256sum order-service-1.0.0.tar离线环境导入:
docker load -i order-service-1.0.0.tar
docker images | grep order-service如果使用 compose,交付包里一般要包含:
docker-compose.yml
.env
外部配置文件
镜像 tar 包
sha256 校验文件
启动脚本
回滚说明内网交付的底线是:换一台干净机器,不访问公网,也能按文档把服务拉起来。
十五、生产排障按现象来
1. 服务启动失败
先看 systemd:
systemctl status order-service --no-pager
journalctl -u order-service -n 300 --no-pager再看应用日志:
tail -n 200 /opt/apps/order-service/logs/app.log常见现象:
Unable to access jarfile:jar 路径错、软链接错、权限不足
UnsupportedClassVersionError:JDK 版本低
Port already in use:端口被占用
Failed to configure a DataSource:数据库配置没加载
Connection refused:依赖服务地址不通对应命令:
readlink -f /opt/apps/order-service/current
java -version
lsof -i :8080
ps -ef | grep '[j]ava'2. 端口被占用
ss -lntp | grep ':8080'
lsof -i :8080如果是旧进程没停掉,先正常停:
kill <pid>等几秒再看:
ss -lntp | grep ':8080' || echo "port released"不要一上来就 kill -9,先确认是不是当前还在承接流量的服务。
3. 配置未生效
ps -ef | grep '[j]ava'
grep -i "profile" /opt/apps/order-service/logs/app.log | tail -n 20查进程环境:
PID="$(pgrep -f 'order-service.*app.jar')"
tr '\0' '\n' < /proc/${PID}/environ | sort | grep -E 'SPRING|SERVER|DB|REDIS|LOG'查配置文件:
ls -l /opt/apps/order-service/config/高频坑:
systemd 没读到 shell export 的变量
实际 profile 不是 prod
外部配置路径写错
配置中心覆盖了本地配置
Docker compose 改了变量但容器没有重新创建4. 日志看不到
先看目录:
ls -ld /opt/apps/order-service/logs
ls -lh /opt/apps/order-service/logs再看 systemd:
journalctl -u order-service -n 200 --no-pager再搜最近日志:
find /opt/apps/order-service -name "*.log" -type f -mtime -1一般原因是:目录没权限、环境变量没传、Logback 没加载、服务启动前就失败、容器只写 stdout。
5. OOM
先判断是 JVM OOM 还是系统 / 容器 OOM。
应用日志:
grep -iE "OutOfMemoryError|Java heap space|Metaspace|Direct buffer memory" /opt/apps/order-service/logs/app.log系统日志:
dmesg -T | grep -i "killed process"Docker:
docker inspect order-service --format='OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'看 dump:
ls -lh /opt/apps/order-service/logs/*.hprof如果没有 dump,回去补 JVM 参数:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/order-service/logs
-XX:+ExitOnOutOfMemoryError这里要注意,OOM 后先保留现场,不要第一时间只加内存。要分清楚是流量突然上来、批量任务过大、缓存不释放,还是代码有内存泄漏。
6. CPU 高或线程卡死
找 Java 进程:
PID="$(pgrep -f 'order-service.*app.jar')"
top -p "${PID}"找高 CPU 线程:
top -Hp "${PID}"把线程 ID 转成十六进制:
printf "%x\n" <tid>抓线程栈:
jstack -l "${PID}" > /tmp/order-service-jstack-$(date +%Y%m%d%H%M%S).log查对应线程:
grep -n "nid=0x<hex_tid>" /tmp/order-service-jstack-*.log一般不要只抓一次,连续抓三次更好:
for i in 1 2 3; do
jstack -l "${PID}" > /tmp/order-service-jstack-${i}.log
sleep 5
done判断方向:
RUNNABLE 且 CPU 高:死循环、复杂计算、JSON 大对象、正则回溯
大量 BLOCKED:锁竞争
大量 WAITING:线程池等待、连接池不足、队列阻塞
大量 HTTP 客户端线程等待:下游接口慢或超时设置不合理
大量数据库线程等待:慢 SQL、连接池耗尽、事务未提交如果允许使用 Arthas,可以这样:
java -jar arthas-boot.jar常用命令:
dashboard
thread -n 10
thread --state BLOCKED
jvm
heapdump /tmp/order-service.hprofArthas 很好用,但生产上要控制权限,不要随便执行会改变业务行为的命令。
7. GC 异常
先看 GC 概况:
PID="$(pgrep -f 'order-service.*app.jar')"
jstat -gcutil "${PID}" 1000 10再看 GC 日志:
tail -n 300 /opt/apps/order-service/logs/gc.log常见判断:
Young GC 很频繁:新对象创建太快、批量请求太大
Full GC 频繁:老年代压力大、堆太小或内存泄漏
单次 GC 停顿长:堆过大、系统负载高或晋升失败
GC 后内存降不下来:存活对象太多,可能有泄漏必要时抓堆:
jmap -dump:format=b,file=/tmp/order-service-heap.hprof "${PID}"抓堆前先看磁盘:
df -h /tmp
df -h /opt/apps/order-service/logs堆 dump 文件可能很大,而且抓取时可能造成短暂停顿。生产执行前要心里有数。
8. 接口超时
先测健康接口:
curl -w "time_connect=%{time_connect} time_starttransfer=%{time_starttransfer} time_total=%{time_total}\n" \
-o /dev/null -s http://127.0.0.1:8080/actuator/health再测业务接口:
curl -w "time_total=%{time_total}\n" -o /dev/null -s http://order.example.com/api/orders如果健康接口快,业务接口慢,优先查业务逻辑、SQL、下游接口。
如果健康接口也慢,优先查 JVM、线程池、GC、CPU、IO。
排查路径可以这样走:
Nginx access log
应用日志 traceId
慢 SQL / 慢外部接口
线程池 / 连接池
GC 日志
系统 CPU / IO / 网络十六、落地工程深水区
Spring Boot 上线最怕的是“应用能启动,但没人知道它能不能长期稳定跑”。生产治理要把 JDK、配置、启动方式、健康检查、日志、回滚和容量放在同一张图里看。
| 深水区 | 现场表现 | 落地做法 |
|---|---|---|
| JDK 与 Boot 版本漂移 | 本地能跑,服务器启动时报字节码或依赖错误 | 发布前固定 JDK 主版本;升级 Spring Boot 时同时校准官方系统要求和依赖矩阵 |
| profile 和配置来源混乱 | 连错数据库、读到测试配置、热修后被覆盖 | 启动命令显式写 --spring.profiles.active;配置文件、环境变量、配置中心版本写入发布记录 |
| systemd 只会拉起进程 | 进程在但服务不可用,异常退出无限重启 | ExecStart、User、Restart、LimitNOFILE 明确;健康检查和日志验证放进发布脚本 |
| 容器资源和 JVM 不一致 | 容器内 OOMKilled,宿主机看内存还够 | 设置容器 memory limit;JVM 参数与容器限制匹配;保留 GC log 和 OOM dump |
| Actuator 暴露过多 | 健康检查方便了,敏感端点也对外可见 | 外网只暴露必要健康接口;管理端口、Nginx location、访问控制分开设计 |
| 日志只写控制台 | 事故后找不到启动参数、traceId、异常上下文 | 应用日志、GC 日志、systemd journal、Nginx access log 分层保留,并带版本号 |
| 依赖就绪检查太浅 | /health 返回 UP,但 DB、Redis、MQ 实际不可用 | readiness 覆盖关键依赖;liveness 不要做重依赖检查,避免误杀 |
| 回滚缺少配置版本 | jar 回滚了,配置中心还是新配置 | jar、配置、数据库变更、开关状态分别有版本和回退步骤 |
| 连接池和线程池未压测 | 低流量正常,高峰 502/504 或超时堆积 | 发布前记录 Tomcat 线程池、DB 连接池、Nginx 超时;按峰值压测后再定参数 |
| 内网/离线依赖不可复现 | 断网后构建失败,或镜像拉不回来 | Maven 私服、基础镜像仓库、JDK 包、构建插件入库;制品用 sha256 校验 |
落地时不要只保存一个 app.jar。至少要能回答:这个 jar 用哪个 JDK 跑、读哪份配置、监听哪个端口、怎么验证健康、日志在哪里、上一个版本怎么退、退回后配置怎么恢复。
十七、上线前检查清单
发布前:
[ ] jar 包能启动,sha256 已记录
[ ] JDK 版本和 Spring Boot 版本匹配
[ ] profile 明确,不靠猜
[ ] 外部配置文件存在,权限正确
[ ] 密码没有写进 Git
[ ] JVM 参数包含 GC 日志和 OOM dump
[ ] 日志目录存在,运行用户可写
[ ] 端口未被占用
[ ] Actuator health 可访问
[ ] systemd 或 Docker 启动方式已验证
[ ] Nginx 已 nginx -t
[ ] 回滚版本和回滚命令已准备发布后:
[ ] systemctl 或 docker 状态正常
[ ] 端口监听正常
[ ] /actuator/health 返回 UP
[ ] 关键业务接口可访问
[ ] 应用日志没有启动异常
[ ] Nginx error log 没有新增 502 / 504
[ ] CPU、内存、GC 没有明显异常
[ ] 当前版本确认无误十八、官方文档入口
平时写脚本可以参考经验,遇到版本差异一定要回官方文档确认。
| 主题 | 文档 |
|---|---|
| Spring Boot 系统要求 | Spring Boot System Requirements |
| 外部配置 | Externalized Configuration |
| Actuator 端点 | Actuator Endpoints |
| 优雅停机 | Graceful Shutdown |
| Maven 插件 | Spring Boot Maven Plugin |
| 可执行 jar | Executable Jar Format |
| Dockerfile | Dockerfile reference |
| Docker 内存 | Docker resource constraints |
| Nginx 反向代理 | ngx_http_proxy_module |
| Maven settings | Maven Settings Reference |
| Maven 离线依赖 | dependency:go-offline |
| systemd service | systemd.service |
| journalctl | journalctl |
| Arthas | Arthas 官方文档 |
最后记住一个生产部署的土办法:每做完一步,就马上验证一步。jar 能不能启动、端口有没有监听、日志有没有写、health 是不是 UP、Nginx 能不能转发、回滚命令能不能执行,这些都不要留到故障现场再补。
