news 2026/9/30 7:58:19

SpringBoot部署到Ubuntu:环境初始化、systemd与排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot部署到Ubuntu:环境初始化、systemd与排错

做Java开发这几年,本地跑SpringBoot应用谁都会——IDEA里点一下Run,浏览器立刻就能看到接口列表,这一套熟练得很。但真正让我觉得自己“进阶了”的事,是第一次把SpringBoot应用从开发机挪到一台干净的Ubuntu服务器上,看着它在一堆异常日志里爬起来,然后稳定跑完整个周末。这篇就围绕这件事来写:在Ubuntu上部署SpringBoot应用,覆盖环境初始化、打包配置、托管启动、部署后的稳定性加固,以及我踩过的坑。适合刚接触服务器部署的Java开发,也适合被“本地能跑、服务器跑不起来”折磨过的人。

部署SpringBoot不是什么玄学,但确实有门槛。这门槛不在SpringBoot本身,而在你对操作系统、进程管理、资源约束的理解。本地开发的默认假设是“我有无限内存、我已经登录了桌面、我随时能看日志”,而生产服务器完全不同。把这些差异想清楚,部署这件事就成功了一半。

1. 部署前的三个决策:选系统、分目录、定运行环境

1.1 为什么生产服务器选Ubuntu LTS而不是其他Linux

先说系统选型。我见过不少团队用CentOS跑Java应用,随着CentOS 8停止维护,很多人被迫迁移。如果你现在还没有既定基础设施,Ubuntu LTS是目前最省心的选择。LTS版本有五年标准支持,云厂商的镜像生态也最完善,遇到问题搜一下基本都有现成答案。

版本上建议直接上22.04 LTS,Java 17配合SpringBoot 3.x完全没有兼容性压力。如果你用的是SpringBoot 2.x,JDK 11也照样跑。选新不选旧的前提是JDK版本跟随SpringBoot的大版本:Boot 3必须JDK 17+,Boot 2.7是老项目,用JDK 8或11都行。

另外,Ubuntu Server默认不带桌面环境,这点对服务器是好事——少装一个桌面就少了一堆依赖和攻击面。说白了,部署Java应用只需要内核、glibc和JVM,桌面环境全是噪音。

1.2 生产与本地开发的三大本质差异

差异决定做法,这些差异理解不到位,后续全在踩坑。

第一,路径。本地你可以在C:\Users\xxx或者/home/xxx下随便放东西,生产服务器必须规划固定目录。应用放哪、配置放哪、日志放哪、临时文件放哪,都要定好规矩。不定规矩的结果就是半年后你自己都找不到jar包在哪个目录。

第二,权限。本地开发时你是管理员,想怎么写就怎么写。生产服务器上不应该用一个root用户跑业务进程。创建专用运行用户、目录按需授权,这才是正确的姿势。进程运行用户权限过大,一旦应用被攻破,攻击者直接拿到root,那就真的完了。

第三,生命周期。本地应用关了IDEA就没了,没人关心开机自启。生产环境要求的是:机器重启后服务自动恢复、日志能轮转、崩溃能被发现。这背后是systemd、logrotate、监控告警这一整套机制,缺一个都不算“部署完成”。

这三个差异就是整篇内容的纲:目录规划解决路径问题,专用用户解决权限问题,systemd和日志治理解决生命周期问题。

2. 环境初始化:JDK安装的两种姿势与Maven镜像配置

2.1 JDK选型与双模式安装详解

JDK是部署的基础,装错了后面全白搭。先说选型结论:OpenJDK足够,没必要上商业发行版。API和Java SE规范完全一致,SpringBoot官方测试也都是基于OpenJDK跑的。版本选择上,SpringBoot 3.x对应Java 17,SpringBoot 2.7对应Java 8或11,见下表。

SpringBoot版本最低JDK版本推荐JDK版本
SpringBoot 3.xJDK 17JDK 17或21
SpringBoot 2.xJDK 8JDK 8或11

安装有两种姿势。第一种是apt直接装,简单但版本受源控制:

sudo apt update sudo apt install -y openjdk-17-jdk-headless java -version

这里我特别用-headless版本,因为服务器不需要图形界面相关库,装了反而多余。第二种是手动解压,灵活性最高,想装哪个版本装哪个版本:

wget https://download.java.net/java/GA/jdk17.0.12/.../openjdk-17_linux-x64_bin.tar.gz sudo mkdir -p /usr/local/java sudo tar -zxvf openjdk-17_linux-x64_bin.tar.gz -C /usr/local/java sudo vi /etc/profile.d/java.sh

/etc/profile.d/java.sh里写入环境变量:

export JAVA_HOME=/usr/local/java/jdk-17.0.12 export PATH=$JAVA_HOME/bin:$PATH

然后source /etc/profile.d/java.sh,java -version验证。为什么推荐第二种?因为apt源里的JDK版本通常滞后,而生产环境锁版本很重要。同一套代码今天用JDK 17.0.8编译,明天服务器上的JDK悄悄升到17.0.11,虽然通常不会出问题,但“可控”这件事在生产环境比“方便”值钱得多。

2.2 构建工具链:Maven与依赖仓库

JDK装好了,接着是构建工具。很多人觉得服务器上不需要Maven,本地打好的jar丢上去就行。这句话只有一半对。服务器保留Maven至少有三个好处:一是应急改配置后可以直接在服务器上重打包,不用每次都从本地传文件;二是排查依赖冲突时可以直接在服务器上跑mvn dependency:tree;三是一些部署流水线脚本需要在服务器侧执行构建。

Maven安装同样建议手动解压,然后配置settings.xml里最重要的镜像仓库:

<mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>

国内服务器不配镜像,第一次构建会卡到怀疑人生——中央仓库的下载速度只能靠运气。实测配了阿里云镜像后,同样一个项目构建时间从十几分钟缩短到两三分钟。

2.3 约定目录结构与专用运行账号

目录规划我有一套固定模板,直接抄就行:

/opt/app/ ├── releases/ # jar包的历史版本,按时间或版本号子目录存放 ├── current/ # 当前运行的jar包软链接 ├── config/ # 外部化配置文件 └── logs/ # 应用日志根目录 /var/log/springboot/ # 存放SpringBoot日志链接或systemd标准输出日志

创建独立运行用户是很多教程忽略的一步:

sudo useradd -M -s /bin/false appuser sudo chown -R appuser:appuser /opt/app

-M表示不创建home目录,-s /bin/false表示禁止登录shell。这个用户只用来跑Java进程,平时根本不需要登录。应用就算被入侵,攻击者也只是个无登录权限的普通用户,不能轻易控制整台机器。

3. 打包与配置分离:让同一个jar包适配任何环境

3.1 SpringBoot的打包配置:jar包能不能直接跑

本地开发的jar包默认是控制台应用,不能直接java -jar跑,必须在pom.xml里配置spring-boot-maven-plugin:

<build> <finalName>order-service</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.OrderServiceApplication</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

配置了repackage后,构建产物会是一个可执行fat jar,里面自带内嵌Tomcat,Java环境有就能跑。这也是SpringBoot部署比传统SSM工程简单太多的原因——不用装Tomcat、不用操心war包容器版本,一个jar就是一个完整应用。

3.2 配置外部化:不再为环境差异反复改代码

有不少人会把数据库密码、Redis地址直接写进application.yml,然后把同一个jar包拷到生产环境,结果启动报错。正确做法是彻底拆分配置。

application.yml只放通用配置,环境相关的全放profile文件:

# application.yml spring: application: name: order-service server: port: 8080
# application-prod.yml spring: datasource: url: jdbc:mysql://内网地址:3306/order_db username: order_app password: ${DB_PASSWORD} redis: host: ${REDIS_HOST}

注意密码用${DB_PASSWORD}占位,从环境变量注入。这样配置文件即使落入他人手中,敏感信息依然是安全的。启动时通过命令指定profile和外部配置目录:

java -jar order-service.jar \ --spring.profiles.active=prod \ --spring.config.additional-location=/opt/app/config/

additional-location会覆盖jar包内的同名配置项,这个功能非常实用——改配置不用重新打包,改完重启就生效。

3.3 构建产物的版本命名

很多人的jar包就叫app.jar,传上去就覆盖,出了故障想回滚根本没得选。我的习惯是带上版本号和构建时间:

mvn clean package -DskipTests cp target/order-service.jar /opt/app/releases/order-service-1.2.3.jar ln -sfn /opt/app/releases/order-service-1.2.3.jar /opt/app/current/order-service.jar

current目录下永远是一个不带版本号的软链接,方便后续systemd或脚本统一引用。部署新版本时,先是新jar落进releases,然后替换软链接,最后重启服务。出问题只需一条命令切回旧版本,成本几乎为零。

4. 部署落地:nohup、systemd与Docker三套方案实测

4.1 nohup直接启动:只适合本地验证和临时调试

最朴素的启动方式是这样:

nohup java -jar order-service.jar --spring.profiles.active=prod > /opt/app/logs/app.log 2>&1 &

nohup让进程忽略挂断信号,&把进程放到后台,输出重定向到日志文件。看起来挺顺,但它的短板很致命:进程失控没有自动重启机制、没有开机自启、进程状态只能靠ps和端口去猜。你说用kill杀错进程或者忘了PID,就够你找一阵子。

这个方案适合什么场景?临时验证一台机器能不能跑通、快速起一个环境跑测试。真要长期服务,下一节的systemd才是正道。

4.2 systemd服务托管:中小项目生产首选

systemd是Ubuntu原生的服务管理器,把SpringBoot注册成服务,开机自启、崩溃重启、日志统一管理全都解决了。在/etc/systemd/system/order-service.service写如下配置:

[Unit] Description=Order Service Application After=network.target mysql.service redis.service [Service] Type=simple User=appuser Group=appuser WorkingDirectory=/opt/app EnvironmentFile=/etc/order-service.env ExecStart=/usr/local/java/jdk-17.0.12/bin/java \ -Xms512m -Xmx512m \ -jar /opt/app/current/order-service.jar \ --spring.profiles.active=prod SuccessExitStatus=143 Restart=always RestartSec=10 StandardOutput=append:/var/log/springboot/order-service.log StandardError=append:/var/log/springboot/order-service-error.log [Install] WantedBy=multi-user.target

逐个解释关键行。After声明依赖顺序,确保网络和数据库先起来。User/Group指定运行身份。EnvironmentFile把数据库密码、Redis地址这些环境变量统一放在独立文件里,权限设为600,与代码完全隔离。Restart=always表示任何非正常退出都自动拉起,实测进程被OOM杀掉、崩溃异常都能在10秒内自动重启。SuccessExitStatus=143是因为systemd认为128+15(SIGTERM)是失败信号,而SpringBoot优雅停机时正好会响应SIGTERM,不声明这个就会把正常停机误判成故障。StandardOutput和StandardError直接打到统一日志文件,不用再手动重定向。

注册并启动服务:

sudo systemctl daemon-reload sudo systemctl enable order-service sudo systemctl start order-service sudo systemctl stordering-service

完整的状态确认要用系列命令:systemctl status看整体状态,ss -tlnp | grep 8080确认端口监听,journalctl -u order-service -f看实时日志。

4.3 Docker容器化:多环境一致性更强

如果你要部署的机器不止一台,或者项目里同时有前端、Redis、MySQL等一堆组件,Docker的价值就出来了。一个Dockerfile搞定环境描述:

FROM eclipse-temurin:17-jre-alpine RUN addgroup -S app && adduser -S app -G app WORKDIR /home/app/ COPY target/order-service.jar app.jar USER app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]

构建和运行:

docker build -t order-service:1.2.3 . docker run -d --name order-service \ -p 8080:8080 \ --restart unless-stopped \ -e DB_PASSWORD=xxx \ -e REDIS_HOST=xxx \ -v /opt/app/config:/home/app/config:ro \ order-service:1.2.3

这里有个镜像选择经验:用jre-alpine而不是完整JDK镜像,体积能小一半不止。SpringBoot运行时不需要编译能力,JRE足够。另外注意--restart unless-stopped策略,容器在机器重启后会自启。

三套方案怎么选?我直接给结论:

维度nohupsystemdDocker
安装复杂度零低中
开机自启需额外脚本原生支持需docker配置
崩溃重启无自动自动
多环境迁移差一般强
运维工具链手动成熟适中
单机多实例麻烦简单简单

单个应用、单台服务器,systemd永远是最省心的选择。需要多环境复制、依赖组件多,选Docker。nohup就当备胎,特殊调试时用一下。

5. 部署后的稳定性加固:反向代理、JVM参数与日志治理

5.1 Nginx反向代理的完整配置

SpringBoot内嵌的Tomcat不适合直接暴露公网,至少应该经过Nginx反向代理,由Nginx处理HTTPS证书、静态资源、访问日志和限流。安装Nginx:

sudo apt install -y nginx

默认站点配置文件在/etc/nginx/sites-available/default,直接改它:

upstream order_service { server 127.0.0.1:8080; keepalive 32; } server { listen 80; server_name orders.example.com; location / { proxy_pass http://order_service; 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 5s; proxy_read_timeout 60s; } access_log /var/log/nginx/order-access.log; error_log /var/log/nginx/order-error.log; }

注意keepalive 32这一行,它让Nginx和Tomcat之间保持长连接,省去每次请求重新握手TCP的消耗。实测压测时这个配置能把Tomcat端的吞吐提升不少。proxy_read_timeout 60s很重要,如果应用有慢接口,默认的60秒超时会导致频繁504,你需要根据接口实际情况调整。

检查配置并重载:

sudo nginx -t sudo systemctl reload nginx

5.2 JVM内存与GC参数调整

部署完应用能启动只是及格,JVM参数调优才是生产环境的加分项。以一台2G内存的服务器为例:

java -Xms512m -Xmx512m \ -XX:MetaspaceSize=128m \ -XX:MaxMetaspaceSize=256m \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -Xlog:gc*:/var/log/springboot/gc.log:time,uptime,level

重点解释几个参数。-Xms和-Xmx设为相同值,防止JVM在运行期动态扩缩内存,减少GC停顿的变量。MaxGCPauseMillis=200给G1设置了GC暂停目标,如果应用响应时间敏感,这个值可以调到100甚至更低,代价是GC频率上升。Xlog:gc*参数输出GC日志,排查内存问题时没它寸步难行。

对于配置了-Xmx的应用,逐步逼近最佳值的方式是看监控:进入稳定运行后,用jstat -gcutil <pid> 1000观察Old区占用量。如果Old区长期稳定在30%以下,可以考虑降内存;如果经常超过70%且回收不干净,就得升内存或排查泄漏了。

5.3 日志轮转与磁盘预警

Java应用最常见的“隐形杀手”是日志撑爆磁盘。SpringBoot默认Logback没有日志文件大小限制,跑上几个月,几个G的日志很常见。用logrotate做轮转,在/etc/logrotate.d/order-service里写:

/var/log/springboot/*.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate dateext dateformat .%Y-%m-%d }

daily表示每天轮转一次,rotate 7保留7份,compress把旧日志压缩成gz,copytruncate保证在不中断Java进程写入的情况下复制日志并清空原文件。dateext让文件名带日期,方便按天回溯。这套配置不需要重启任何服务,logrotate是cron自动调度,每天跑一次。

磁盘预警建议配合简单的cron脚本,比如df -h / | awk '{print $5}'判断使用率,超过85%就发个通知。部署阶段的最后一个环节,就是把这些加固项全部加上。到此应用才算真正“入了生产”。

6. 生产排错实录:我踩过的SpringBoot部署坑与修复链路

6.1 服务器重启后服务不见了:忘记enable的经典失误

这个坑几乎每个新手都会踩。当时我用systemd把服务跑起来,systemctl start一切正常,接口通、日志干净。然后运维通知重启机器,我满心欢喜等着服务自启,等来的却是端口没监听。

排查链路是这样的:先systemctl status order-service显示inactive (dead),再systemctl is-enabled order-service显示disabled。这就是答案——我只执行了start,没有执行enable。start只管当前运行,enable才是注册开机自启。

修复很简单,sudo systemctl enable order-service一次搞定。这个坑告诉我们一个检查原则:systemd管理服务时,start和enable是两个动作,忘一个都不行。部署检查清单里永远要写一句“确认is-enabled不是disabled”。

6.2 服务反复重启又消失:OOM Killer的介入

第二个坑是跑着跑着进程突然没了,systemd一直在拉起,但拉起后几秒又死。我用systemctl status看不到具体报错,日志里也没有异常堆栈。最后用dmesg -T | grep -i kill把内核日志翻出来,看到一行:Out of memory: Killed process 12345 (java)。

这是典型的物理内存不足,触发了Linux OOM Killer。1G内存的机器,-Xmx512m的堆、256M的Metaspace、加上Tomcat的线程栈和内嵌容器自身开销,实际RSS轻松超过1G。系统连内核自己的运行内存都保不住时,只能杀进程。

修复方案分两层。第一层给系统加swap作为缓冲:

sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile

然后在/etc/fstab里写一行,保证开机自动挂载swap。第二层是砍掉JVM非必要内存,把-Xmx调低到物理内存的一半,并加上-XX:MaxMetaspaceSize=128m。内存不够的服务器别硬撑,要么加内存要么缩JVM,两者都能解决问题,但优先推荐把JVM调整到合理范围。

6.3 Java版本不一致带来的连环问题

第三个坑比较隐蔽。本地开发用的JDK 17,打包没问题。服务器上装的是JDK 11,本以为向下兼容,结果启动直接报UnsupportedClassVersionError: org/springframework/boot/loader/Launcher has been compiled by a more recent version of the Java Runtime。

这个错误信息本身已经把原因说清楚了:类文件编译版本高于运行版本。SpringBoot 3.x编译目标是17,JDK 11的JVM根本不认识class文件版本61。之所以在部署时容易忽略,是因为大家普遍有“高版本JDK能跑低版本class,那低版本也能跑高版本”的错觉——前者对,后者完全不对。

排查链路按顺序记录:先java -version确认JVM版本为11,再javap -verbose order-service.jar | grep "major"查class文件主版本号是61。结论清晰,卸载JDK 11、安装JDK 17后解决。

这类版本问题还有一个变种:编译目标版本和运行版本一致,但某些API在运行时不可用。比如用JDK 17的压缩字符串优化特性,然后跑在JDK 11上,会报UnsupportedOperationException。排查思路是看第一次抛出的异常在哪个JDK类库方法里,再对照当前JVM版本的特性支持列表。部署前在服务器上执行一次编译,或者用jdeps扫描jar包的JDK依赖,能提前避免大部分问题。

6.4 端口被占用与防火墙拦截

SpringBoot默认8080端口,新部署时经常遇到Port 8080 was already in use。排查方式:

ss -tlnp | grep 8080

拿到占用进程后,如果是旧实例,systemctl stop对应服务;如果是来路不明的进程,kill前先确认它是什么。这里我的经验是:不要盲目kill -9,先用ps -fp <pid>看启动命令,确认不是别人家的服务。

防火墙是另一个容易忽略的点。Ubuntu默认用的是UFW,端口明明监听成功,外部却访问不到,先查:

sudo ufw status sudo ufw allow 8080/tcp

如果是在云服务器上,别忘了安全组里也要放行对应端口。本地防火墙、云安全组、Nginx配置,这三层任何一层没放行,外部都访问不了应用。排查外网不通时按这个顺序查,比乱试一通效率高得多。

排错的核心思路说到底就一句话:从日志和系统状态里找线索,而不是靠猜测。journalctl看systemd日志,dmesg看内核日志,ss看端口,jstat看JVM内部状态,df看磁盘。工具链齐了,问题大多能定位到具体环节。我在实际部署中还有一个习惯:每次排完一个坑,就把报错信息、排查过程和修复方案记到一个本地文档里。几次之后你会发现,大部分生产故障都是重复的,真正的新问题反而很少。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 7:57:55

Unity 接入海康热成像 SDK:温度矩阵解析与伪彩热图渲染

"热成像测温"这四个字落到 Unity 项目里,通常意味着三件事:一台海康的测温型热像仪、一个已经跑起来的三维场景、以及一个必须在两周内把温度数字画到模型上的需求。Unity 本身完全不懂热成像,它只认纹理、网格和 Shader;而海康的测温相机只会通过自己的 SDK 往外吐字…

作者头像 李华
网站建设 2026/9/30 7:56:49

Spring Boot家教预约管理系统:从数据库设计到并发控制的实战全解析

做家教兼职管理系统那会儿&#xff0c;很多人说这类毕设项目就是“换个壳的CRUD”&#xff0c;但真把业务跑通之后我才发现&#xff0c;这个题目比表面看起来有嚼头得多。尤其是带着“补习班预约”这个功能&#xff0c;它涉及完整的多角色权限、状态机流转、时间冲突判断&#…

作者头像 李华
网站建设 2026/9/30 7:56:46

平台+AI重塑软件交付生态,成长型伙伴迎来第二增长曲线

前几天我在一个行业群里看到有人转发了摩尔元数2026成长型生态伙伴大会的消息&#xff0c;当时就对"平台AI"这个主题挺好奇。等真正把大会内容完整看完&#xff0c;又和几位参会的区域伙伴聊了一圈&#xff0c;我意识到这场大会传递的信号&#xff0c;可能比它本身的…

作者头像 李华
网站建设 2026/9/30 7:56:12

Windows 11记事本卡死深层解析:TSF、AppContainer与UI.Xaml阻塞链

1. 这不是记事本的问题&#xff0c;而是Windows 11底层服务链的“微小脱节” 你双击桌面快捷方式&#xff0c;光标转圈三秒&#xff0c;窗口边框刚浮现就卡死&#xff1b;右键新建→文本文档&#xff0c;图标悬停半秒后直接消失&#xff1b;任务管理器里notepad.exe进程CPU占0%…

作者头像 李华
网站建设 2026/9/30 7:55:32

微信API高效解析:Java对象映射与Jackson配置优化实战

做微信生态开发的同学应该都遇到过这种场景&#xff1a;调用微信API返回一大段JSON&#xff0c;字段命名是下划线风格&#xff08;比如nickname、openid&#xff09;&#xff0c;嵌套层级很深&#xff0c;时不时还冒出个负数错误码。要高效解析这些数据并映射到Java对象&#x…

作者头像 李华
网站建设 2026/9/30 7:54:41

PyTorch环境搭建与LoRA微调实战:从版本兼容到显存优化

1. 从零搭PyTorch环境&#xff1a;版本漂移比模型本身还难调上周帮一位做机器狗控制的朋友调LoRA训练脚本&#xff0c;他刚装完PyTorch&#xff0c;兴致勃勃准备微调一个7B模型&#xff0c;结果第一步就卡住了——import torch没问题&#xff0c;但torch.cuda.is_available()一…

作者头像 李华