资讯动态

SpringBoot项目部署云服务器全流程:IDEA打包上传与systemd守护实战

发布时间:2026/10/16 13:31:52 来源:尧图企业网站定制
本地启动SpringBoot项目三秒钟页面就出来了接口测试全绿。可一旦要发布到云服务器各种问题就接踵而来Maven打包出来的jar动辄几十兆用scp传上去慢得不行好不容易传上去了重启之后发现端口没开端口开了访问又超时最后能访问了跑两天又莫名其妙挂掉。这些场景我太熟悉了刚接触部署的那阵子几乎每次发布都要折腾半小时以上。这篇文章把我平时用Intellij IDEA配合云服务器以阿里云ECS为例其他云厂商的服务器操作完全通用部署SpringBoot项目的完整链路拆开讲一遍覆盖选型、打包、上传、进程守护、日志排查和故障优化。适合第一次做部署的开发者也适合已经会部署但想规范发布流程、减少线上事故的人。1. 选型与前置准备别等打包完才发现环境不对部署这件事一半的问题在部署动作本身另一半在部署前的决策。很多人上来就打包、上传结果环境不匹配、端口没开、内存不够白白折腾。先把基础设施想清楚后面会顺很多。1.1 服务器规格、系统镜像和运行环境SpringBoot项目自带内嵌Tomcat本质上就是一个独立进程对服务器的要求不高。以我常用的单体应用为例1核2G的实例跑一个小型管理后台完全够用。如果你准备在服务器上同时部署MySQL、Redis、Nginx甚至还要跑Docker那么2核4G起步会更从容否则内存分配会很紧张。系统镜像我建议选主流版本比如CentOS 7.9、Rocky Linux或者Ubuntu 22.04。云厂商提供的公共镜像一般都优化过内核参数直接能用。选系统的时候有个容易被忽略的点如果你是照着网上的旧教程操作注意区分命令体系。CentOS 7用yumUbuntu用aptRocky Linux用dnf别混着来。JDK版本方面SpringBoot 2.x用JDK 8或11都很稳SpringBoot 3.x必须用JDK 17以上。很多人踩过这样的坑本地用JDK 17开发服务器装的是JDK 8jar包传上去一执行就报UnsupportedClassVersionError。这个错误从报错信息看挺吓人但本质就是编译版本和运行版本对不上。解决方式也很简单要么服务器装和本地一致的JDK要么在pom.xml里指定maven.compiler.source和maven.compiler.target同时把SpringBoot版本和JDK版本匹配好。1.2 安全组规则端口没放通部署全白费云服务器的网络安全机制和本机不太一样。你本地起一个8080端口直接用localhost就能访问但在云服务器上即使项目成功启动了外网也不一定连得上。原因就在于云平台的安全组规则它相当于一层外部防火墙默认只放行少量端口。以阿里云ECS为例安全组规则在控制台的“实例→安全组→配置规则”里配置。常见的安全组放行方案如下端口用途建议放开范围22SSH远程连接仅限你自己的办公网IP或0.0.0.0/0配合强密码和密钥登录8080SpringBoot应用主端口可按需放开或通过Nginx反向代理只放803306MySQL数据库禁止对公网开放只允许内网或特定IP访问6379Redis禁止对公网开放必须绑定内网IP并设置密码实际操作中很多人只改了云服务商控制台的安全组却忘了服务器本身还有一套防火墙。CentOS 7自带firewalldUbuntu自带ufw如果安全组放行了端口但系统防火墙没放行一样访问不了。我建议你在服务器上执行systemctl disable firewalld或者用firewall-cmd --permanent --add-port8080/tcp把端口加进去二者选其一避免排查时精神错乱。这个“双重防火墙”的问题我见过太多次了配置安全组和系统防火墙时要一起检查。1.3 安装JDK、MySQL的基础环境思路JDK的安装没有太多花头优先用系统包管理器装而不是手动下载tar包。在CentOS/Rocky上执行yum install -y java-11-openjdk装完用java -version验证。这里提醒一句如果你机器上曾经装过别的版本可能要把JAVA_HOME环境变量写进/etc/profile否则后续systemd启动脚本可能找不到Java路径。MySQL的安装稍微复杂一点。如果你不想折腾官方Yum源直接用云厂商提供的云数据库产品是最省心的毕竟云数据库自带高可用、自动备份和监控告警比自己在ECS上维护MySQL省太多事。如果是用来学习和测试那么在自己的ECS上装一个MySQL 8.0也完全可以yum install -y mysql-server systemctl start mysqld grep temporary password /var/log/mysqld.logMySQL 8.0初始化的临时密码会写进日志文件首次登录后必须改密码。这里要特别提一下数据库字符集一定要在建库时就设置成utf8mb4不然后面存emoji或生僻字会出现乱码数据都写进去了再改字符集非常麻烦。2. 打包方式的取舍与多环境配置服务器和环境准备好之后下一步就是把本地项目变成可交付的产物。SpringBoot有两种常见的打包方式jar和war。很多人纠结这个其实在云服务器部署场景下结论非常明确。2.1 为什么我推荐打jar而不是warwar包是传统Java Web项目的产物目标是部署到外部Servlet容器如Tomcat、Jetty里Web容器是外置的项目本身不负责启动服务。而SpringBoot默认使用内嵌Tomcat打出来的jar包自己包含启动逻辑一条java -jar命令就能把整个应用跑起来。在云服务器部署时jar包有明显的优势交付简单一个jar就是整个应用不依赖额外的Tomcat安装和配置。启动可控JVM参数、内嵌端口、profile都可以通过启动命令指定不需要改容器配置文件。目录干净不涉及webapps的放置逻辑备份和回滚也只针对一个文件。用war包也有适用场景比如你的服务器上已经有一套成熟的Tomcat集群统一管理多个应用或者做运维的同事对Tomcat管理特别熟。但对于大部分个人项目和中小团队项目jar包加systemd的部署方式是最容易维护的。在pom.xml中确保打包方式是jar并配置好finalName这样产出的文件名是固定的方便后续脚本处理build finalNameapp-server/finalName plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build2.2 用Maven Profile隔离开发和生产配置SpringBoot项目最常见的部署事故就是开发环境和生产环境的数据库地址、Redis地址混在一起。本地连本地数据库打包后却把本地数据库地址带到了服务器上服务起不来或者数据写到本地电脑里去了。这种问题不是小概率是大概率。正确做法是用Maven Profile做环境隔离。在application.yml里指定当前激活的profilespring: profiles: active: profile.active这个profile.active会被Maven在打包时替换成具体的值。然后在pom.xml中定义多个profileprofiles profile iddev/id properties profile.activedev/profile.active /properties /profile profile idprod/id properties profile.activeprod/profile.active /properties /profile /profiles在src/main/resources目录下建立application-dev.yml和application-prod.yml分别填写各自的数据库连接、Redis地址、日志级别等。打包的时候执行mvn clean package -Pprod -DskipTests这样打出来的包里面的主配置就是prod环境。有一个细节值得注意如果你的构建机器和被部署的服务器不在同一个内网数据库地址要写云数据库的公网地址或者通过SSH隧道连接不要写内网地址导致本地测试能跑、服务器上连不上。2.3 打包过程的常见报错打包报错主要集中在以下场景这里列一下我遇到过的-DskipTests不生效这是skipTests和maven.test.skip的区别。-DskipTests是跳过测试执行但会编译测试代码-Dmaven.test.skiptrue才是连测试代码编译都跳过速度更快。打包后的jar无法执行多半是缺少spring-boot-maven-plugin的repackage目标。如果你用的是自定义parent要显式声明goalrepackage/goal否则打出来的jar只是普通jar运行时会报“no main manifest attribute”。配置文件的profile.active没有被替换检查pom.xml里是否有resources配置导致资源过滤没有生效。如果配置了resources标签要把${basedir}/src/main/resources加回来并开启filtering。3. IDEA远程部署从上传jar到远程重启环境没问题包也能打出来接下来就是本文的重头戏怎么用Intellij IDEA把jar部署到云服务器并且把整个发布过程自动化。这一节的方法是我日常的主力方案完全基于IDEA内置能力不需要额外安装付费插件。3.1 先用SSH密钥打通本地和服务器密码登录虽然简单但每次执行远程命令都要输密码自动化流程就卡住了。所以我强烈建议配置SSH密钥登录。你可以在本机生成密钥对云控制台一般也支持导入公钥或直接创建密钥对。如果已经生成过密钥直接复用就行。以Linux和Mac本机为例ssh-keygen -t ed25519 -C your-emailexample.com一路回车后会生成~/.ssh/id_ed25519.pub公钥。把公钥内容追加到服务器~/.ssh/authorized_keys文件里mkdir -p ~/.ssh echo ssh-ed25519 AAAA...(你的公钥内容) ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys然后用ssh userserver_ip测试是否能免密登录。这一步打通之后后面的IDEA配置和远程命令执行都会非常顺畅。如果你用的是Windows本机也可以借助系统自带的ssh-keygen步骤是一样的。配置SSH密钥还有一个额外的好处可以彻底关闭服务器的密码登录降低暴力破解的风险。这套安全习惯建议从一开始就养成算是部署的基本素养。3.2 配置Deployment实现一键上传IDEA自带的Deployment功能可以理解为一个可视化SFTP客户端。路径是Tools → Deployment → Configuration点击加号新建一个Server类型选SFTP。配置项看起来多真正需要关心的就三个地方Connection填服务器IP、端口22、用户名、认证方式选择密钥或密码。建议填私钥文件的路径。Root Path这个目录是你在IDEA里看到的远程根目录我习惯填/home/项目部署用户/。Mappings映射务必把本地打包输出目录target映射到远程目录。比如本地target里的app-server.jar可以直接上传到远程的/opt/apps/upload/这个临时目录。配置完成后点击Test Connection测试连通性。接下来在Deployment窗口的“Upload”标签页里就能看到本地和远程的目录对应关系手动右键文件或目录就可以上传。这一套基本操作起来非常直观比用scp输命令高效得多。这里要明确一个理念上传的jar不应该直接覆盖正在运行的jar。最好的做法是上传到一个临时目录然后用脚本完成“备份→替换→重启”三个动作。这样做的好处是如果新版本启动失败还能快速回滚到上一个可用版本。3.3 用运行配置把打包、上传、重启串起来Deployment能省掉手动上传的功夫但打包、上传、远程重启还是三个动作。要真正实现一键发布需要利用IDEA的Run Configuration组合能力。我的做法分两种场景场景A有时间打开IDEA慢慢发布在IDEA左上角的运行配置下拉框里新建一个Maven配置command line填clean package -Pprod -DskipTests。再新建一个Shell Script配置执行内容就是一条远程命令ssh userserver_ip bash /opt/apps/deploy.sh然后把这两个配置加到同一个Compound组里顺序是先Maven再Shell Script这样每次发布只需要点一次运行。Upload那一步虽然还是手动但已经把最容易出错的打包和重启自动化了。场景B想让发布动作更自动化IDEA其实也可以让Deployment的Upload动作触发在Run之前。在Compound配置里可以把一个“运行外部工具”的步骤加进去指向IDEA的Deployment上传。不过这个配置方式不同版本IDEA略有差异建议你用我上面那个方案简单直接也不容易出配置问题。重点在于服务器端的deploy.sh脚本这个脚本写得好不好直接决定发布流程的稳定性。#!/bin/bash APP_NAMEapp-server.jar APP_DIR/opt/apps UPLOAD_DIR/opt/apps/upload BACKUP_DIR/opt/apps/backup LOG_DIR/opt/apps/logs # 确保目录存在 mkdir -p $APP_DIR $UPLOAD_DIR $BACKUP_DIR $LOG_DIR # 备份当前版本 if [ -f $APP_DIR/$APP_NAME ]; then cp $APP_DIR/$APP_NAME $BACKUP_DIR/${APP_NAME}-$(date %Y%m%d%H%M%S).bak echo 备份完成: $(date %Y%m%d%H%M%S) fi # 替换新版本 cp $UPLOAD_DIR/$APP_NAME $APP_DIR/$APP_NAME chmod x $APP_DIR/$APP_NAME # 重启应用用 systemd 管理时 systemctl restart app-server echo 应用重启完成这个脚本的逻辑很简单先把当前运行中的jar归档到backup目录时间戳命名方便回溯再用上传目录的新jar覆盖正式目录最后触发systemd重启服务。脚本里每一步都打印日志部署过程中出了任何问题都能很快定位。还要提醒一个容易踩的坑上传jar时如果网络不稳定可能传了一个不完整的文件结果是把坏包覆盖到正式目录导致服务起不来。所以在复制前校验文件大小或MD5是一个值得做的增强点。我一般在上传前对比一下本地和远程的文件大小确认一致再执行替换。4. 服务器端进程守护与日志排障上传和重启只是部署的第一步真正考验一个服务的是“能不能稳定跑着”。很多人用nohup把进程启动起来就万事大吉了但这样做的隐患很隐蔽。4.1 nohup启动方式与不足最简单的启动方式是这样nohup java -jar /opt/apps/app-server.jar /opt/apps/logs/app.log 21 这条命令的意思是用nohup让进程在终端退出后继续运行标准输出和错误输出都重定向到日志文件最后面的表示放到后台。这种方式的不足之处在于如果进程意外退出了没有机制自动拉起服务器重启后进程不会跟着启动查看进程状态也好、做优雅停机也好全靠人工命令。对于临时测试没问题但只要是面向正式环境的服务都别用nohup裸跑。你会陷入“每天早上到公司发现服务挂了然后手动拉起来”的尴尬循环。4.2 用systemd管理SpringBoot服务systemd是现代Linux系统的服务管理器用它来管SpringBoot进程比nohup规范得多。在/etc/systemd/system/目录下新建服务文件比如app-server.service[Unit] DescriptionApp Server Service Afternetwork.target [Service] Userroot WorkingDirectory/opt/apps ExecStart/usr/bin/java -Xms256m -Xmx512m -jar /opt/apps/app-server.jar --spring.profiles.activeprod SuccessExitStatus143 Restartalways RestartSec10 [Install] WantedBymulti-user.target这里有几个配置项值得说明Restartalways进程异常退出时systemd会自动拉起间隔时间是10秒。SuccessExitStatus143这是Java应用优雅停机时的退出码。没有这行的话systemctl stop可能会被标记为失败虽然不影响使用但日志里会多出错误记录。ExecStart里的JVM参数-Xms256m -Xmx512m是堆内存的起始和最大值需要根据服务器物理内存调整。1核2G的机器给Java堆512M比较合理给太多会挤占操作系统内存给太少会频繁GC。配置完成后执行systemctl daemon-reload systemctl enable app-server systemctl start app-server用systemd管起来之后日常操作变成systemctl restart app-server、systemctl status app-server发布脚本的可靠性也上了一个台阶。4.3 日志、端口和进程状态的三板斧排查服务启动失败时最忌讳一头扎进代码里改逻辑。先按顺序做三件事大部分问题都能定位。第一板斧看日志journalctl -u app-server -f tail -f /opt/apps/logs/app.logsystemd会把服务的标准输出记录下来用journalctl可以直接查看最近日志。如果日志是写到文件里的那tail -f实时跟踪。注意SpringBoot的启动日志会明确提示程序是否成功启动比如“Started Application in x.xx secondsJVM running for x.xx”看到这个基本就成功了大半。第二板斧看端口netstat -tlnp | grep 8080 ss -tlnp | grep java端口监听状态不对说明应用没有完全起来或者被别的进程占了。一个典型的坑是8080被第二次启动的进程占用导致新的服务实例报Port already in use。这种情况多半是上一次的进程没有完全停止停服务前先确认一下进程是否真的退出。第三板斧看进程ps -ef | grep app-server如果进程在反复重启说明启动时抛了异常被systemd的Restartalways疯狂拉起又失败。此时日志就是关键线索来源重点看有没有APPLICATION FAILED TO START这个标志性段落下面会直接列出启动失败的原因。我把这三步总结成一个先看日志、再看端口、最后看进程的排查顺序实际使用中能覆盖掉90%的部署启动问题。5. 一次真实线上故障的排查实录与优化工具和流程都讲完了用一次我实际处理过的线上故障作为收尾。故障本身不复杂但排查思路和事后优化对部署运维有通用参考价值。5.1 现象刚上线半小时服务就变慢某个内部管理项目部署环境是1核2G的ECS部署完前半小时一切正常然后接口响应越来越慢最后直接卡死。第一次遇到这种情况肯定会怀疑是代码死循环或者别的服务把CPU打满了。上服务器先看CPU和内存top -bn1 | head -20实际看到的情况是Java进程CPU接近200%内存占用一直居高不下GC线程非常活跃。这个表象说明系统在做大量垃圾回收回收完了内存又立刻被占满周而复始。这是典型的年轻代内存太小或对象分配过快导致的GC瓶颈。5.2 排查链路从日志到堆栈到配置先用jstat看看GC情况jstat -gcutil pid 1000 10结果很明显Young Generation的GC非常频繁而且每次GC后占用率又迅速回到高位。接着用jstack抓到进程的线程快照看是否有锁竞争jstack pid threaddump.txt最终真正的问题定位在数据库连接池参数上。项目用的是MySQL数据库但连接池最大连接数配置成了200而数据库服务器的max_connections只有128。大量请求都卡在等待数据库连接上线程一直占着内存不释放导致堆内存涨满GC也救不回来。本质上就是资源超卖应用层配置的并发上限远远超出了数据库层能承受的上限。日志里其实有大量类似HikariPool-1 - Connection is not available, request timed out after 30000ms的报错排查初期我只盯着服务日志的ERROR级别没注意到这个超时警告。这个细节很值得记住HikariCP连接池超时是java.util.concurrent超时异常不一定按ERROR级别输出如果忽略WARN级别日志很容易漏掉真正的根因。5.3 事后总结部署前就该改掉的三个参数这个故障之后我把项目的相关参数整理成一份部署检查清单每次发布前都要过一遍配置项推荐值原因SpringBoot内嵌Tomcat最大线程数100~200默认200对小服务器偏大线程多内存占用就高HikariCP最大连接数20~50单体应用用不到太多数据库连接设太大反而拖垮数据库JVM堆内存根据机器配置分配1核2G推荐-Xms256m -Xmx512m预留内存给OS和MySQLTomcat线程和数据库连接池要一起算账一个请求占一个Tomcat线程这个线程去数据库时又要占一个连接。如果Tomcat线程数设200、连接池设200一旦并发上来数据库先扛不住然后所有线程都阻塞在那里内存自然就吃满了。正确的做法是让Tomcat线程数略大于数据库连接数让数据库连接成为可控的瓶颈而不是无限膨胀。顺着这个思路后来我又给项目加了一个简单的健康检查接口返回200加上数据库连通性的探活信息配合云监控的拨测告警在用户抱怨之前就能发现服务异常。我在实际部署中发现发布链路里的绝大多数问题都不是“单点技术深奥”而是“多个环节的参数不匹配”。环境版本、内存分配、连接池参数、安全组端口这些细节单独看都微不足道组合在一起就是线上事故的温床。把部署流程脚本化、参数清单化之后发布这件事才能真正省心。如果你也在反复处理部署问题建议先别急着改代码把服务器到IDEA到应用进程的整条链路重新梳理一遍收获会比想象中更大。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑