SpringBoot项目部署,听起来就是个老生常谈的话题,但真正自己动手把服务扔到Linux服务器上跑起来的时候,坑一点都不少。环境变量没配好、端口被占了、内存不够启动失败、数据库连不上、日志刷屏,几乎每个环节都有人踩过。我这几年部署过不少模拟项目和线上系统,从最早的裸机直接跑,到后来配合systemd做守护、再演进到容器化部署,每个阶段都折腾出了不少经验。这篇就专门聊SpringBoot项目部署这件事,把从打包到启动、从环境准备到问题排查的完整链路讲清楚,给正在准备上线的同学一份可以直接照着操练的参考。
1. 部署前先想清楚:方案选型比敲命令更重要
很多人拿到SpringBoot项目,第一反应就是mvn package然后java -jar跑起来。这没错,但如果只停留在这一步,后面迟早会吃亏。部署这件事,真正花时间的不是那条启动命令,而是你选择用哪种方式把这个服务稳定地跑起来,以及出了问题之后能不能快速定位。
1.1 Jar包还是War包:这不是二选一的选择题
SpringBoot官方默认推荐的方式是打包成可执行的Jar包,因为SpringBoot内置了Tomcat容器,一个java -jar就能直接启动,不需要额外在服务器上装Tomcat。这种方式的优势非常明显:环境依赖少、启动逻辑统一、升级的时候只需要替换一个文件,而且Jar包可以被systemd、Docker、K8s这些上层工具统一管理,非常适合微服务架构里一个服务一个进程的部署模型。
War包则适用于另一类场景:公司已经有了统一的Tomcat集群,或者运维体系要求必须走应用服务器的标准发布流程。这种情况你就要把SpringBoot的打包方式改成War,同时还得让启动类继承SpringBootServletInitializer并重写configure方法。但说实话,现在新项目还坚持外部部署Tomcat的已经越来越少了,War包部署最大的问题是把容器管理和应用生命周期耦合在一起,出了问题很难分清是应用挂了还是容器出毛病了。
我自己在实际部署中,Jar包和War包都用过。如果是全新的、自运维的项目,直接用Jar包;如果是那种必须接入已有中间件管理平台的老系统,才考虑War包。有一点要特别提醒:SpringBoot的spring-boot-maven-plugin在打War包的时候会自动排除内嵌Tomcat,但前提是你的打包配置写对了,否则打出来的War包既带内嵌容器又被外部容器加载,启动时会直接报端口冲突或者容器初始化失败。
1.2 部署目标环境:先把这几件事弄清楚再动手
部署之前先花十分钟确认目标服务器的环境参数,这个时间花得特别值。主要看四样东西:操作系统、JDK版本、中间件版本、硬件资源。
操作系统方面,生产环境绝大多数是Linux,而且主流是CentOS系或Ubuntu系。不同发行版在安装JDK、配置systemd、用yum还是apt装依赖时都有差异,别拿一套命令在两个体系里来回套。我踩过的坑是,CentOS用firewall-cmd放行端口,Ubuntu要用ufw,写启动文档的时候一定要标注清楚操作系统类型。
JDK版本这个问题更关键。SpringBoot 2.x对Java 8依然有很好的支持,但如果你用的是SpringBoot 3.x,那要求JDK 17起步。另外,SDK版本的不匹配会导致类加载异常、方法不存在这类恶心问题,看日志还特别难发现。建议部署前用java -version确认一下,如果服务器上装了多个JDK,就设置好JAVA_HOME和PATH,让Java命令和${JAVA_HOME}指向同一个版本。
中间件版本指的是MySQL、Redis、MQ这些外部依赖的版本。比如MySQL 5.7还是8.0,直接决定了驱动依赖的写法——MySQL 8.0要求JDBC驱动使用com.mysql.cj.jdbc.Driver,同时还要处理时区参数serverTimezone。Redis则要确认客户端的序列化方式跟服务端配置是不是一致,不然连上了也会吐一堆二进制乱码出来。
硬件资源这块最简单,但最容易漏。两三台机器虽然都能跑起来,但JVM默认堆大小是根据物理内存自动算出来的,如果服务器内存就2G,SpringBoot默认堆可能就直接顶到物理内存的一半,再加上系统本身占用的部分,很容易触发OOM。所以部署前心里要有个底:这台机器准备给这个服务多少内存和多少核的CPU,后面JVM参数就按这个数来配。
1.3 部署方式矩阵:裸机、守护进程、容器化怎么选
我把SpringBoot常见的部署方式分成三个梯队,每个梯队对应的运维成本和适用场景完全不同,直接决定你要不要写systemd配置、要不要写Dockerfile。
第一个梯队是最简单的java -jar裸跑,scp上去执行命令就完了。启动方便但缺点也很明显:终端一关进程就没了,宕机没人拉起来,日志没人管,重启还要人肉登录服务器敲命令。这种适合一次性验证、临时演示,绝不建议拿来跑线上。
第二个梯队是systemd守护进程方式,这是我在Linux服务器上最推荐的部署姿势。systemd既能管开机自启,也能监控进程状态,进程挂了还能自动拉起,日志交给journald统一收集。SpringBoot的Jar包部署成systemd服务,一套unit文件写好后从此不用再手敲启动命令,这块后面我会专门讲配置。
第三个梯队就是容器化部署,Docker和K8s。好处是环境隔离、镜像不可变、弹性扩缩容,缺点是引入了镜像构建、网络模型、存储卷这些额外的概念,对纯做业务开发的同学来说学习成本并不低。如果你的项目还处于单机部署阶段,没必要一上来就上K8s,先把systemd玩明白更实际。
下面的表格是我在部署时会做的粗略对比,方便你根据实际情况选型:
| 部署方式 | 进程守护 | 开机自启 | 自动拉起 | 扩展性 | 运维成本 |
|---|---|---|---|---|---|
| 裸跑java -jar | 无 | 无 | 无 | 差 | 低 |
| systemd服务 | 有 | 有 | 有 | 中 | 中 |
| Docker容器 | 有 | 需配置 | 需配置 | 较好 | 中高 |
| K8s编排 | 有 | 有 | 有 | 强 | 高 |
2. 环境准备与项目配置:部署前最后一次检查
选好了部署方案,接着就是准备工作。这一步看起来琐碎,但恰恰是线上问题的高发地带。我见过太多次因为application-prod.yml里配了个开发环境数据库地址,导致上线后服务连不上数据库然后启动失败的案例。所以环境准备的核心思路只有一条:把开发和生产的差异尽量挡在部署动作之前。
2.1 JDK安装与多版本管理
如果你的服务器是全新的,第一步先把JDK装好。用系统自带包管理器安装是最省事的,比如CentOS上yum install java-1.8.0-openjdk,Ubuntu上apt install openjdk-17-jdk。装完之后记得验证一下:
java -version javac -version echo $JAVA_HOME这三条命令的输出要互相匹配。很多问题都出在java -version是新的,但JAVA_HOME还指向老路径,导致Maven或者脚本打包时用的却是另一套环境。
如果服务器上有多个JDK版本并存,用alternatives命令来管理,可以随时切换默认版本:
alternatives --config java alternatives --config javac我在实际部署中遇到过这样一个问题:项目是SpringBoot 3.x要求JDK 17,但服务器上部署的另一个老项目必须用JDK 8。两个服务同时跑在同一台机器上,处理办法是每个服务单独写启动脚本,在脚本里显式指定JAVA_HOME,不依赖系统全局变量。
还有一点,OpenJ9和HotSpot是两套不同的JVM实现,虽然都能跑Java字节码,但参数和行为差异不小。SpringBoot项目的部署默认用HotSpot就好,性能稳定,资料也最多。
2.2 数据库与中间件的连接配置
SpringBoot项目部署,大概率躲不开外部依赖,最常见的两个就是MySQL和Redis。先聊MySQL。
如果你用的是MySQL 8.0,pom.xml里引入连接驱动时,建议直接上mysql-connector-j这个新版坐标,旧坐标mysql-connector-java虽然还能用,但官方已经停止维护了。配置文件里最关键的是URL上的参数,一个典型的生产库配置是这样的:
spring: datasource: url: jdbc:mysql://your-db-host:3306/app_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: app_user password: your_strong_password driver-class-name: com.mysql.cj.jdbc.Driver这里面的serverTimezone=Asia/Shanghai基本是必填的,否则JDBC驱动和MySQL服务端时区不一致,操作DATETIME类型经常报错或者拿到的时间差八小时。useSSL=false是针对内网数据库的常见配置,除非你确实做了SSL通道,否则加上这个避免SSL握手带来的额外耗时和证书报错。
Redis的配置相对简单,但要留心密码和数据库编号:
spring: data: redis: host: your-redis-host port: 6379 password: your_redis_password database: 0 timeout: 3000msdatabase: 0是默认值,但如果多个应用共用同一个Redis实例,务必用不同的库编号或者更合理地用不同的key前缀做隔离。我遇到过部署时把生产环境的Redis配到了开发环境库,结果缓存数据互相覆盖,排查了半天才发现是database配置错了。
2.3 配置文件的Profile管理与外部化配置
SpringBoot的多环境配置,核心就是application.yml+application-{profile}.yml这套机制。项目里一般会有dev、test、prod三套环境配置,部署时通过启动参数告诉SpringBoot激活哪一套:
java -jar app.jar --spring.profiles.active=prod或者用环境变量SPRING_PROFILES_ACTIVE=prod。这两种方式等价,环境变量的方式在systemd和Docker里更好管理。
但这里有个细节非常容易坑人:配置的优先级。SpringBoot的配置来源优先级从高到低大概是命令行参数、环境变量、外部配置文件、Jar包内部配置文件。也就是说,如果你在启动命令行里写了--spring.profiles.active=prod,那外部的application-prod.yml可以覆盖Jar包内部的同名配置。这个机制可以用来在部署时不重新打Jar包、临时变更某个数据库地址之类的需求。
生产环境配置里肯定有敏感信息,比如数据库密码、Redis密码、第三方密钥。把这些信息直接写进application-prod.yml放进代码仓库并不安全,更稳妥的做法是使用环境变量占位符:
spring: datasource: username: ${DB_USERNAME} password: ${DB_PASSWORD}然后在部署环境里通过systemd的Environment字段或者Docker的-e参数注入。这样代码仓库里只留占位符,真实密码只存在于服务器环境中,就算代码泄露也不会直接炸掉生产库。
3. 打包、上传与启动:一步步把服务跑起来
环境准备好了,接下来进入正题:把你的SpringBoot项目从开发机搬到服务器上,并让它稳定运行。这一章我会把从Maven打包到最后服务正常启动的全过程完整过一遍,重点放在那些容易出细节问题的环节。
3.1 Maven打包:跳过测试与多环境构建
打包的第一步是保证本地代码能正常编译。在项目根目录执行:
mvn clean package -DskipTests -Pprod解释一下这一条命令里两个关键的参数。-DskipTests代表跳过单元测试执行,但依然会编译测试代码——如果你希望连测试代码编译都跳过,建议用-Dmaven.test.skip=true,这个更狠,编译和运行都跳了。另一个参数-Pprod表示激活Maven的prodprofile,这个profile会触发对应环境下的资源过滤,把application-prod.yml中的@profile.name@这类Maven占位符替换成实际值。
打完包后,在target/目录下会生成xxx.jar和xxx.jar.original两个文件。很多第一次部署的人看到xxx.jar.original会一脸懵,其实这个文件是SpringBoot插件重新打包之前的原始Jar包。如果你只需要跑一个可执行版本,部署时认准不带.original的那个就行了。
这里有个需要注意的地方:如果pom.xml里配置了spring-boot-maven-plugin的finalName,那生成的Jar包名是固定的,不会带版本号。设置一个不含版本号的固定文件名,比如app.jar,对部署脚本来说更友好,因为后续升级只需要替换文件,不用修改systemd或脚本里的文件名。
打包这个环节还有个常见的坑:本地能打包成功,服务器上总是构建失败。大概率是Maven的settings.xml里配置的私服地址不同,或者依赖在本地仓库有缓存而服务器上拉取不到。解决方案是在服务器上也配好统一的Maven仓库配置,并且不要轻易把本地target目录里的东西直接scp到服务器,最好在构建机上从源码重新打包,保证产物的一致性和可追溯性。
3.2 首次启动与启动日志解读
Jar包上传到服务器后,第一件事不是配置systemd,而是先手动启动一次,把启动日志看明白,确认服务真的能起来。
java -jar app.jar --spring.profiles.active=prod > app.log 2>&1 &这里> app.log 2>&1是把标准输出和标准错误都重定向到app.log文件。&是让Java进程在后台运行。这个命令适合临时验证,但不适合长期跑,因为进程和ssh会话挂在一起,终端断开容易出问题。
启动之后,重点看两类日志。第一类是SpringBoot的启动日志,如果配置正常,你会看到类似Tomcat started on port(s): 8080 (http)这样的关键行,这说明内嵌Tomcat已经成功绑定端口。第二类是应用日志,看到Started Application in x seconds才算启动完成。如果有APPLICATION FAILED TO START,直接把日志里对应的描述搜出来就能定位问题。
为了减少干扰,第一次启动调试时可以把日志级别调成DEBUG看更详细的信息。但要注意,生产环境千万别全程DEBUG跑,日志量太大,磁盘很快就被打满了。通常的做法是部署验证时临时用--debug参数或者修改profile里的日志级别,确认完立刻改回来。
3.3 进程守护与开机自启:systemd实战
手动启动验证通过后,就该把它交给systemd了。在/etc/systemd/system/下新建一个service文件,假设服务名就叫app.service:
[Unit] Description=SpringBoot Application After=network.target [Service] Type=simple User=appuser WorkingDirectory=/opt/app Environment=JAVA_HOME=/usr/local/jdk17 Environment=SPRING_PROFILES_ACTIVE=prod Environment=DB_USERNAME=app_prod Environment=DB_PASSWORD=your_strong_password ExecStart=/usr/local/jdk17/bin/java -Xms512m -Xmx512m -jar /opt/app/app.jar Restart=always RestartSec=5 SuccessExitStatus=143 [Install] WantedBy=multi-user.target逐个字段说一下我是怎么理解的。Type=simple表示ExecStart启动的进程就是主进程,这也符合SpringBoot Jar包直接用Java命令启动的场景。WorkingDirectory设置的是工作目录,SpringBoot项目里如果代码用了相对路径来读写文件,这个字段会直接影响路径解析。Restart=always表示进程退出后无条件重启,配合RestartSec=5是设置5秒后拉起,能应对不少偶发崩溃。
SuccessExitStatus=143是个很多人看不懂的配置。SpringBoot应用在systemctl stop时,systemd会发送SIGTERM信号,SpringBoot优雅停机后进程退出码是143,这个退出码在systemd眼里默认表示失败。加上这个字段告诉systemd:退出码143是正常行为,不要误报。
配置完成后,依次执行:
systemctl daemon-reload systemctl enable app systemctl start app systemctl status appenable是注册开机自启,start是立即启动。status输出的结果里如果看到active (running),就说明服务已经成为系统管理的常驻进程了。
这里还提供一个非常有用的运维技巧:SpringBoot项目配合Spring Boot Actuator来做健康检查。在pom.xml里加上spring-boot-starter-actuator依赖后,访问http://localhost:8080/actuator/health会返回JSON格式的健康信息。这个端点可以配合systemd来写自定义的HealthCheck脚本,每隔一段时间探测一次,发现不健康就自动重启服务,相当于给健康检查加了一层保险。
4. 常见问题与排查技巧实录
部署SpringBoot项目的过程中,有些坑几乎人人都会踩一轮。我把这些年遇到的典型问题按类别整理成下面这份排查手册,每一个都是我实际处理过的场景,排查思路也都是验证过的,可以直接拿来用。
4.1 端口被占用:最平常但也最烦的启动失败
SpringBoot启动时报端口占用的错误信息特别明显,日志里会直接告诉你Port 8080 was already in use。这时候先确认是谁占了端口:
netstat -tlnp | grep 8080 lsof -i:8080这两条命令都能查到PID。然后根据PID反查进程信息:
ps -ef | grep <PID>查完之后分两种情况处理:如果是你自己的旧进程没杀干净,直接kill -9 <PID>,注意kill -9是强制杀掉,如果进程需要优雅停机,最好先用kill -15给进程一个处理信号的机会;如果是别的服务占用了端口,就改SpringBoot的server.port,或者调整占用方的端口。
我实际排查时遇到过一种隐蔽情况:端口被一个Java进程占着,但lsof显示的进程名是java,看半天不知道是哪个服务。这时候可以查一下这个进程的启动命令和启动参数:
cat /proc/<PID>/cmdline | tr '\0' ' '看到启动参数里的Jar包路径大概就能判断是哪个应用了。部署多个SpringBoot应用时,强烈建议每个应用固定不同的server.port,然后在systemd的unit文件里通过Environment=SERVER_PORT=8081注入,避免不同实例之间互相抢端口。
4.2 内存不足与JVM参数导致的问题
这类问题分两种情况:一种是真的物理内存不够,另一种是JVM启动参数配置不合理。
物理内存不够直接的表现是进程启动到一半被系统杀掉,用dmesg | tail查看内核日志,常会看到Out of memory: Kill process。另一种启动时的报错是Error occurred during initialization of VM / Could not reserve enough space for object heap,说明JVM按照默认的堆大小向系统申请内存,但系统拿不出这么多连续内存。
排查思路是先看当前可用内存:
free -h如果总内存就2G,那JVM默认堆大小可能直接按物理内存的1/2来算,也就是1G甚至更多。这时候要在启动参数里显式控制内存:
java -Xms256m -Xmx512m -jar app.jar-Xms是初始堆大小,-Xmx是最大堆大小。经验值是把这两个参数设成一样的数值,避免运行期间JVM反复扩容和收缩带来的性能抖动。比如一个常规业务系统给512MB堆基本够用了,如果你的服务有大的缓存需求或者高并发场景,可以适当调大到1G。
Docker部署的场景还要额外注意:如果你用的JDK版本较老,JVM不认识容器环境里的cgroup限制,-Xmx设置得很大也没用,JVM还是会按照宿主机内存来计算默认堆。这种情况下必须在启动命令里显式指定-Xmx,不能依赖默认值。
4.3 数据库连接失败与驱动配置问题
数据库导致的启动失败,错误日志大多数是Communications link failure或者Access denied for user。逐一排查三个点:网络、账号、驱动。
先确定数据库地址网络能通:
telnet your-db-host 3306本地通不了,项目部署在另一台服务器理所当然是连不上的。能通但还是连不上,就要看URL里的时区、SSL配置和账号密码对不对。
有一类问题经常被忽略:MySQL 8.0数据库在默认的caching_sha2_password认证插件下,老版本的JDBC驱动无法适配,会报认证失败。如果你的项目是从MySQL密码认证升级过来的,在pom.xml里确认驱动版本,低版本的驱动建议升级到8.x系列。
数据库连接池这边也有讲究。SpringBoot默认的HikariCP性能很好,但有几个参数部署时最好显式配置:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 max-lifetime: 1800000max-lifetime是连接的最大存活时间,如果数据库连接是经过中间代理或防火墙的,某些中间件会把长时间空闲的连接默默断掉,客户端这边还拿着旧连接往数据库发SQL,就会报Connection is not available或者Broken pipe。这时调整HikariCP的max-lifetime比数据库wait_timeout稍微短一些能有效规避这个问题。
4.4 上传文件的路径问题与资源映射
SpringBoot部署后,文件上传相关的报错和静态资源找不到,排查起来往往要比数据库问题更费神,因为错误信息不太直观。
如果你在代码里用相对路径保存上传文件,比如new File("./upload"),那文件落在哪里取决于运行时的工作目录。用systemd部署时,工作目录是WorkingDirectory指定的值,不是Jar包所在目录。这就是一个经常踩的坑:明明在本地跑得好好的,部署到服务器上就找不到上传的文件,偶尔还报FileNotFoundException。
我的经验是:在application.yml里用一个绝对路径配置:
app: upload-dir: /data/app/upload代码里通过@Value("${app.upload-dir}")注入这个路径,然后启动前用脚本提前创建好这个目录:
mkdir -p /data/app/upload chown -R appuser:appuser /data/app/upload注意权限问题,运行SpringBoot进程的systemd用户如果不是root,就要确保这个目录对运行用户有读写权限,这一条对数据目录、日志目录都适用。
静态资源访问不到的另一个常见原因是没有做资源映射。SpringBoot的静态资源默认放在classpath:/static/下,但如果你把文件传上去期望它出现在某个可访问的URL路径下,就需要在配置里加一个资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:/data/app/upload/"); } }这里的/upload/**是外部访问的URL路径,file:/data/app/upload/是映射到的本地物理路径。
5. 日志管理与性能调优经验
服务启动稳定了,不代表部署这件事就结束了。真正考验一个部署方案好坏的标准,在于能不能支撑日常运维:日志要能查、参数要能调、状态要能看。这一章分享一些我在日志处理和JVM调优时的实际做法。
5.1 日志输出策略与日志文件切割
SpringBoot默认用Logback作为日志框架,但很多项目一开始只把日志打印到控制台,生产环境里这是不够的。部署上线时,至少要保证日志有两份去路:一份输出到文件以便留存,一份输出到控制台以便systemd的journald收集。
日志文件切割是必修课,不然一个日志文件无限增长,几天就能把磁盘打满。一个标准的Logback配置,文件大小到指定值就自动滚动,并且保留最近N份:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/data/app/logs/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>/data/app/logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> </rollingPolicy> </appender>maxHistory是保留最近30天的日志,maxFileSize是每个文件达到100MB就滚动。这样配好后,大文件日志会被自动按天按大小切分成小文件,方便用grep定向搜索。
还有个更简单的方案:如果你用systemd管理服务并且没有单独的日志文件需求,可以直接不用Logback的RollingFileAppender,统一看journalctl -u app的日志输出,systemd本身就会做日志的按时间轮转和大小限制。二选一就够了,别同时开两套容易把磁盘写爆。
5.2 JVM参数与性能观测
JVM参数的合理配置,对SpringBoot应用的稳定性和性能影响很大。这里给一个相对中庸、适合大多数中小型系统的组合:
java -Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxMetaspaceSize=256m -jar app.jarUseG1GC是JDK 8之后比较推荐的垃圾回收器,相比CMS它的暂停时间更可控。MaxMetaspaceSize限制的是元数据空间,防止加载过多类导致内存失控。如果你的应用有大量缓存,比如用了本地缓存,JVM堆设置得大一些比较合适;依赖于Redis做缓存的服务,JVM堆就没必要给得太多,够用就行。
调优的一个重要前提是能看到监控数据。最简单的方式是打开JMX远程端口,用JConsole或者VisualVM连过去看堆内存、GC频率、线程数。但生产环境开JMX要小心安全风险,最好只监听内网地址或者用SSH隧道转发。
不想开JMX的话,Spring Boot Actuator的/actuator/metrics接口也能暴露JVM的关键指标,比如堆内存使用量、活跃线程数、GC次数,通过定时采集这些指标,再配合告警就能对服务的健康状态有一个清晰的感知。
5.3 启动脚本与日常运维细节
最后分享一个我一直在用的启动脚本模板,它做的事情包括:检查Jar包是否存在、检测端口是否已被占用、用nohup启动服务并写入PID文件、通过tail跟踪启动日志。这个脚本适合在项目还没接入systemd时的临时运维场景,或者作为Docker容器的entrypoint、打包发布时手动执行的辅助脚本:
#!/bin/bash APP_NAME=app APP_JAR=/opt/app/app.jar LOG_FILE=/data/app/logs/app.log PID_FILE=/var/run/app.pid start() { if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") if kill -0 "$PID" 2>/dev/null; then echo "App already running, pid=$PID" exit 1 fi fi nohup java -Xms512m -Xmx512m \ -jar "$APP_JAR" \ --spring.profiles.active=prod > "$LOG_FILE" 2>&1 & echo $! > "$PID_FILE" echo "App started, pid=$(cat "$PID_FILE")" } stop() { if [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") kill "$PID" rm -f "$PID_FILE" echo "App stopped, pid=$PID" else echo "PID file not found" fi } case "$1" in start) start ;; stop) stop ;; restart) stop; sleep 2; start ;; *) echo "Usage: $0 {start|stop|restart}"; exit 1 ;; esac这个脚本里kill -0是个非常实用的检查方式:它不发送真正的信号,只探测进程是否存在。配合PID文件来管理进程状态,比每次都用pgrep再过滤进程名要可靠得多。
6. 部署后的完整验收清单
部署完成后,千万别看一眼active (running)就觉得大功告成了。我习惯跑一遍下面这张验收清单,确认服务在功能之外的状态也是健康的,避免上线以后被用户发现各种隐性故障。
6.1 端口与进程状态检查
先确认进程是否存在,系统服务是否正常注册:
systemctl status app ss -tlnp | grep 8080如果systemd显示active但端口没有监听,极有可能是启动失败但被systemd自动拉起了,或者还在启动过程中。此时Follow一下日志:
journalctl -u app -f重点看有没有异常的堆栈、超时或连接失败。确认无误后,再做一个HTTP级别的探测,用curl访问某个接口:
curl -I http://localhost:8080/api/some-endpoint curl http://localhost:8080/actuator/health如果返回200或者正常的JSON响应,端口这一关才算真正通过。
6.2 配置生效确认
检查当前生效的profile和外部配置是否如预期,方式有很多种,最简单的就是看Actuator的/actuator/env。这个端点会列出所有配置来源及其优先级,可以确认--spring.profiles.active和systemd里注入的Environment是否真的写进了生效配置。
生产环境如果不想暴露这个端点,可以通过日志佐证。比如在启动日志里搜索The following 1 profile is active: "prod",确认激活的确实是prod而不是dev。这一步非常关键,我曾经见过一次事故:Jar包内的application.yml默认激活了dev profile,启动命令里的--spring.profiles.active=prod因为参数顺序问题没生效,导致服务连上了开发数据库,虽然运行正常但数据完全错了。
6.3 数据落库与文件权限验证
最后一步是验证服务的读写权限。找一个测试接口,或者直接在数据库里执行一次写操作确认账号授权没问题,再测试一下文件上传路径是否可写。如果在启动脚本里已经用chown把目录交给了运行用户,一般没问题,但还是要实测一下。
磁盘空间也要顺手看一眼,之前遇到过日志目录没挂在数据盘上,结果日志把系统盘占满导致服务无法启动的事故,用这些命令查:
df -h du -sh /data/app/logs留给日志和依赖文件足够的空间,比任何调优都重要。
7. 写在最后:一些真正帮你省钱省时间的经验
做SpringBoot部署这件事本身不复杂,复杂的是环境差异和细节疏忽。我在多次部署中最大的体会是:部署最大的敌人不是不会敲命令,而是想当然。
想当然地觉得Jar包在本地能跑就能在服务器上跑,想当然地觉得配了数据库地址就能连上,想当然地觉得进程在跑就一切正常。每一层想当然,最后都得花几个小时去查日志、看监控才能找补回来。
我的建议很简单:给自己写一份部署Checklist。从环境准备到打包上传、从启动验证到健康检查,每一步都逐项打勾。这个习惯一旦养成,部署出错率会直线下降。哪怕你换了一家公司、换了一套基础设施,只要这份清单的核心还在,新环境部署也就是改改地址、调调参数的事。
另外一个强烈推荐的做法:每次部署完,把操作过程和遇到的问题用一两段话记录下来。不用写得很正式,真实为主,记录这次改了哪个配置、重启了几次、为什么那个参数要那样配。积累半年之后,这份笔记就是你自己最靠谱的运维手册。我这些年排过的大部分坑,最后发现都能在自己沉淀的笔记里找到类似案例。
原本想最后再聊点别的,但部署这件事实在是实践出真知。只要你多部署几次、多看几份日志、多踩几个坑,自然就有手感了。下次再有人问我在服务器上跑一个SpringBoot服务该怎么做,我大概会说:先把环境搞清楚,再把这篇文章里的细节过一遍,最后不要忘记给服务配一个可靠的守护进程。剩下的,就是你跟日志之间的交情了。