☰
SpringBoot项目部署到云服务器全流程:从打包到公网访问
2026/10/14 7:13:27 网站建设 项目流程

做后端这几年,SpringBoot项目的部署基本成了家常便饭,尤其是项目验收前、临时出环境、演示给客户看的时候,总要把本地跑得好好的项目挪到云服务器上。这个“挪”的过程,看起来就是打个包、丢上去、启动一下,真操作起来却有一堆细节:JDK版本不对、端口被占、安全组没放行、启动后进程莫名其妙退出……每一步都可能卡你半天。

我写这个系列教程,就是想把SpringBoot项目部署到云服务器上这件事拆开揉碎讲清楚。第一篇只做一件最重要的事:让你本地能跑的项目,在一台全新的云服务器上,通过一条干净的启动命令真正跑起来,并且能通过公网访问到。适合刚接触云服务器、没怎么碰过Linux的小白,也适合之前一直用图形化面板、没自己敲过部署命令的后端开发。整个过程我会按照实际动手顺序来讲,顺带把常见坑和排查思路都放进去。

1. 部署方案设计:动手前想清楚这四件事

1.1 一次完整部署到底包含哪些环节

很多人第一次部署,拿到服务器后直接就开始装Java、传包,传上去运行报错了才发现少了这个少了那个。我现在的习惯是,先在心里把整条链路过一遍:本地代码打包成可运行产物、准备服务器环境(系统、JDK、目录、权限)、上传产物、配置生产环境参数、执行启动命令、验证服务状态、设置日志和进程退出后的处理。这样一步步做,每步都有明确输出物,排错也容易定位。

这就像搬家,代码是行李,云服务器是新房子,JDK这些环境是水电。你不能先把行李丢进去再开始通水电,而是先确认房子基础没问题,再逐步布置。部署也一样,顺序错了,后面全是问题。

这个系列一共会分几篇,第一篇只聚焦从零到“服务在服务器上稳定跑起来”,不涉及数据库接入、Nginx反向代理、域名绑定、HTTPS证书这些进阶内容。先把基础打牢,后面再往上叠东西,否则一上来就铺太多概念,出了问题根本分不清是哪一层引起的。

1.2 服务器配置怎么选才不会踩坑

如果你是从零买服务器,配置上我建议初期就选择2核4G起步,内存低于2G跑SpringBoot会比较吃力,尤其是项目里用了Redis、MySQL或者依赖比较重的时候。带宽选按量付费或者3M到5M的小带宽就够用了,第一次部署主要是传包和访问测试,不需要太大。

操作系统方面,我推荐选Linux发行版,CentOS 7系或者Ubuntu 20.04/22.04都是主流,云厂商默认镜像里都有。你不需要两个都学,选一个用熟即可。我自己长期用CentOS,但这两年新机器也经常选Ubuntu,因为命令更新、软件源更活跃。教程里的命令我会同时标注yum和apt两种写法,你根据系统对号入座。

地域选择有个小经验:服务器地域选在离你目标用户近的地方,比如你服务主要给国内用户访问,就别选海外区域,不然延迟和回源链路都会让人难受。另外,新服务器刚买的时候,系统盘默认给的40G到60G,对部署一个SpringBoot项目来说完全够用,不用额外加钱扩容。

1.3 打包成jar还是war,怎么选最省心

SpringBoot项目有两种常见打包方式:jar和war。很多人一开始不清楚区别,其实核心在于内嵌服务器:打成jar包,项目里已经内置了Tomcat,直接java -jar就能跑;打成war包,通常要把它丢到外置的Tomcat的webapps目录里,由外置容器加载。

我的建议是:没有特殊要求,一律打成jar包。理由很简单——部署命令短、依赖隔离干净、版本升级只需要换一个jar文件,回滚也方便。war包适合老项目迁移、企业里已有统一Tomcat集群的场景,或者你非要在同一台机器上部署多个应用并想共享Tomcat实例,但那种情况通常用Docker更合适,而不是war包。

如果你用的Spring Boot 3.x,打包默认就是jar,且要求JDK17。Spring Boot 2.x系列则兼容JDK8到JDK11。很多公司生产环境还在用JDK8,所以选版本时先确认你的Spring Boot版本能匹配哪一代JDK,避免到了服务器上启动直接报UnsupportedClassVersionError。

1.4 云厂商安全组和服务器防火墙的区别

我第一次上云时,最懵的就是“安全组”和“防火墙”到底管哪个。简单说,云厂商的安全组是云平台层面的第一道闸门,在虚拟机外面;服务器内部的iptables或者firewalld是第二道闸门,在系统里面。两个都得放行,你的端口才真正对公网开放。

我遇到过很多次这种情况:服务器上SpringBoot已经启动,curl localhost:8080也正常返回,但浏览器访问公网IP就是超时。最后检查,安全组里压根没有放行8080端口。所以后面部署时,我习惯先确认安全组入方向规则,再确认机器内部防火墙,顺序不要搞反。

2. 服务器环境准备:给程序一个干净的家

2.1 连接服务器后,先别急着装东西

拿到一台全新服务器,我做的第一件事永远是更新系统软件源和已有包。CentOS执行sudo yum update -y,Ubuntu执行sudo apt update && sudo apt upgrade -y,让系统基础软件处于较新状态,避免后面安装依赖时碰到源里的旧版本包。

然后是创建专用账号。我见过不少同学图省事一直用root部署,运行应用时也直接root。问题是如果应用被攻击或者你执行了危险命令,root权限下影响面太大。我更建议创建一个普通用户,比如就叫deploy,专门用来存放代码、启动进程。命令大致是:useradd deploy,再设置密码或配置SSH密钥。后面所有和项目相关的文件都放在这个用户的家目录下,管理清爽很多。

同时创建一个合理的目录结构,我习惯这样规划:

  • /opt/app:存放jar包和启动脚本
  • /opt/app/logs:存放应用运行日志
  • /opt/app/backup:存放历史版本包

这样做的最大好处是:以后升级只需要把新的jar丢进/opt/app,改一下软链或者脚本里的文件名,旧版本留在backup里可以快速回滚。目录不乱,排查问题时心态就不容易炸。

2.2 JDK版本选择与安装

JDK版本选择直接决定项目能否启动。你本地用JDK11开发,服务器上装了个JDK17,短期可能没问题,但有些框架的字节码版本就不兼容。反过来本地用17、服务器用8,那基本启动就会报错。

我个人推荐的生产组合:

Spring Boot版本JDK版本说明
2.7.xJDK8 / JDK11使用最广,JDK8足够
3.0.x及以上JDK17官方最低要求,部分库需同步升级

安装JDK,最简单的方式是用系统包管理器:

# CentOS sudo yum install java-1.8.0-openjdk-devel -y # Ubuntu sudo apt install openjdk-17-jdk -y

如果系统源里找不到你想要的版本,可以去官网下载tar.gz包,解压后自己配JAVA_HOME。不管哪种方式,装完都建议验证一下:

java -version javac -version

这里提醒一点:云服务器上尽量不要装“最新版JDK”,除非项目明确用了新特性。生产环境稳定才是第一位的,JDK8和JDK11这种长期支持版本维护时间长、社区资料多,踩坑概率低很多。

2.3 环境变量和JVM参数怎么配

安装完JDK,如果你用的是包管理器安装,一般会自动配置好符号链接,java命令直接能用。如果是手动解压的tar.gz包,需要把JAVA_HOME写进/etc/profile或者用户目录的.bashrc:

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

然后执行source /etc/profile让它生效。这里我特别想说一下JVM参数的问题,很多人启动命令就是java -jar demo.jar,项目也能跑,但一旦并发上来或者日志量大,经常出现卡顿和频繁GC。我给自己的部署脚本里加了一组初始参数:

java -Xms256m -Xmx512m -Xss256k -jar demo.jar

-Xms和-Xmx分别表示堆内存初始值和最大值,对于一台2G内存的服务器,跑一个小型SpringBoot服务,堆设成256m~512m比较合适。要是把-Xmx设成1g,加上JVM其他内存区域和系统本身,2G内存就会很紧张。原则是:给JVM留够空间,但不要把所有物理内存都塞给它。

3. SpringBoot项目打包与上传:把本地代码变成云端程序

3.1 Maven打包前的配置环境检查

在本地打包前,我习惯先在pom.xml里看一眼打包插件版本,确认Spring Boot Maven插件存在,因为它会把依赖全部打进去,生成真正可执行的fat jar。核心配置大致如下:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>

同时检查application.yml里有没有把数据库密码、各种密钥写死。如果项目里有test目录,打包时不想执行测试用例,可以在命令里加-DskipTests。这句的意思是不运行测试代码,但依然编译测试类;如果想连编译都跳过,可以用-Dmaven.test.skip=true。我平时习惯用前者,保留编译过程能快速发现接口签名问题。

另外必须注意:Maven的settings.xml里配置的镜像源不同,打包速度可能差很多。本地打包遇到依赖下载缓慢或者失败,优先检查配置的镜像地址。这些和云服务器本身无关,但是部署链路的第一环,也值得提前排掉。

3.2 生产环境的配置文件到底要不要单独维护

一个高可维护的部署流程,不会每次发版都去改代码里的配置。我更建议在项目里放不同环境的配置文件,比如application-dev.yml、application-prod.yml,然后在启动命令里通过--spring.profiles.active=prod指定使用哪套配置。

生产环境配置里至少要包含:

  • 日志输出路径,比如/opt/app/logs/app.log
  • 数据库连接地址、用户名、密码(密码建议用环境变量引用)
  • 线程池、连接池等参数设为适合生产的值

我常用的启动命令形式是这样的:

nohup java -jar demo-service.jar \ --spring.profiles.active=prod \ --server.port=8080 \ > /opt/app/logs/console.log 2>&1 &

这里有一个容易被忽略的点:jar包旁边其实可以放一个外部application-prod.yml,Spring Boot会优先读取当前目录下的外部配置,优先级高于jar包内部。这样改配置也不用重新打包,发版成本低很多。

3.3 上传jar包到服务器的几种方式

本地打包完成后,在target目录下会生成一个几十兆甚至上百兆的jar文件。上传到服务器,最简单的就是scp命令。我在本机终端执行:

scp target/demo-service-1.0.0.jar deploy@服务器IP:/opt/app/

如果你是Windows环境,也可以用终端工具自带的上传功能,或者SFTP客户端,效果一样。上传完成后,我建议先做一个校验,避免文件传输过程中损坏:

# 本地生成MD5 md5sum target/demo-service-1.0.0.jar # 服务器上校验 cd /opt/app && md5sum demo-service-1.0.0.jar

两个结果一致,再继续下一步。文件损坏这种事不常见,但一旦发生,启动时会抛出莫名其妙的zip END header错误,排查起来很头疼。提前花十秒校验,能省掉后面半小时的怀疑人生。

4. 首次启动与进程守护:从命令行到可控服务

4.1 nohup启动命令解析

在服务器上启动SpringBoot项目,最简单的就是nohup java -jar xxx.jar &。我先把完整命令写给新手看:

cd /opt/app nohup java -Xms256m -Xmx512m \ -jar demo-service.jar \ --spring.profiles.active=prod \ > /opt/app/logs/app.log 2>&1 &

一个个拆开解释含义。nohup的意思是让进程忽略挂断信号,即使你关闭SSH连接,进程也不会因为终端退出而被系统杀掉。&表示让命令在后台运行。> logs/app.log 2>&1表示把标准输出和错误输出都写入同一个日志文件,避免启动日志丢失。

如果你不加nohup,关闭终端的时候,进程很可能收到SIGHUP信号直接退出。这大概是最常见的“启动成功了,退出SSH后再访问就超时”的原因。

启动后,第一时间看日志确认启动状态:

tail -100f /opt/app/logs/app.log

正常会看到Tomcat started on port(s): 8080或者Started Application in xxxx seconds这类信息。看到这些,说明Spring Boot已经在运行了。接着可以用本机回环地址测试:

curl http://localhost:8080/你的接口路径

返回正常,才能说明服务真正可用。

4.2 端口、防火墙与云安全组的联合排查

如果curl本地都返回结果,但从你自己电脑浏览器访问公网IP:8080不通,问题几乎一定出在两个地方:云安全组或服务器内部防火墙。

先在服务器内确认端口监听状态:

netstat -tunlp | grep 8080

或者用:

ss -tunlp | grep 8080

如果看到java进程LISTEN且IP是0.0.0.0:8080,说明应用监听没问题。接下来检查内部防火墙:

CentOS7及以上:

sudo firewall-cmd --list-all # 如果8080不在列表里,永久放行: sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent sudo firewall-cmd --reload

Ubuntu系统:

sudo ufw allow 8080/tcp

然后去云厂商控制台找到安全组/防火墙策略,添加入方向规则:协议TCP、端口8080、来源建议先填0.0.0.0/0或我的公网IP/32。我自己的习惯是先只允许自己IP访问,调试通过后再按需放宽。

这一节看着简单,但它是新手部署失败率最高的地方。记住我前面说的:安全组和服务器防火墙是两个层面,漏掉任何一个都不行。

4.3 如何优雅地停止和重启应用

启动容易,停止反而有讲究。我早期图省事直接kill -9 PID,虽然进程立即没了,但如果有正在处理的请求、缓存里的数据还没落库,就可能造成数据异常。所以现在我的流程是先优雅停止,再兜底强杀。

找到进程ID:

ps -ef | grep demo-service | grep -v grep

优雅停止用:

kill PID

Spring Boot会监听到SIGTERM信号,然后按照规定流程关闭容器、释放资源。如果等了四五秒还没退出,再执行kill -9 PID强杀。

这里我还想说一个守护细节:进程挂掉后,如果没有监控机制,服务就是悄悄消失。我见过很多同学启动完就不管了,第二天发现服务挂了,日志停在某一条报错上。最简单的临时方案是写一个看门狗脚本,用crontab每分钟检查一次端口,发现没有就在/opt/app目录下重新拉起进程。后面系列教程如果讲systemd托管服务,会比看门狗脚本更规范,但第一版能用起来更重要。

5. 常见问题与排查技巧实录

5.1 端口被占用导致启动失败

日志里看到Port 8080 was already in use,说明端口被别的进程占用了。先用lsof -i:8080或者netstat -tunlp | grep 8080找到占用进程是谁。如果是旧版本应用还在跑,用前面说的kill方式停掉再重启;如果是其他无关进程占用了端口,要么换SpringBoot启动端口,要么处理掉那个进程。别遇到过就直接改端口,先去搞清楚为什么被占才是对的。

5.2 编码问题导致日志乱码或接口返回乱码

中文项目在服务器上经常出现日志中文乱码,核心是Java启动时的默认编码和系统环境编码不一致。Linux系统默认locale可能是POSIX或者UTF-8,不一致时就会出现乱码。我在启动命令中显式加上:

-Dfile.encoding=UTF-8

同时确保日志文件写入时,logback.xml里对应的encoder charset是UTF-8。这个小参数加和不加,生产环境里真的能差出很多麻烦。

5.3 内存不足导致启动直接失败

如果你在启动日志里看到:

There is insufficient memory for the Java Runtime Environment to continue

说明系统内存不够JVM分配。这种情况常见于1G内存的服务器,而你的-Xmx又设大了。优先调整JVM参数调小,或者给系统增加swap空间应急:

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

但要注意,swap只适合应急,应用真正稳定运行还是得有足量物理内存,否则频繁换页反而更慢。

5.4 防火墙放行后仍然无法访问

有时安全组也放行了、firewalld也没限制,但公网还是访问不了。这种时候按顺序检查云控制台里的入方向规则是否生效,有些厂商修改安全组规则后需要等待一小段时间;再确认服务器上的应用监听地址是不是0.0.0.0,而不是127.0.0.1。后者表示只在本机监听,外网自然进不来,需要在启动命令或配置里明确监听所有网卡。

5.5 一套可用的启停脚本模板

把上面这些经验沉淀成脚本,部署时就不用每次都敲一长串命令。我日常用的启动脚本长这样,放在/opt/app/start.sh:

#!/bin/bash APP_NAME=demo-service.jar LOG_DIR=/opt/app/logs RUN_USER=deploy cd /opt/app if [ ! -d "$LOG_DIR" ]; then mkdir -p "$LOG_DIR" fi PID=$(pgrep -f "$APP_NAME") if [ -n "$PID" ]; then echo "进程已存在,PID: $PID" exit 1 fi nohup java -Xms256m -Xmx512m \ -Dfile.encoding=UTF-8 \ -jar "$APP_NAME" \ --spring.profiles.active=prod \ > "$LOG_DIR/console.log" 2>&1 & echo "启动成功,PID: $!"

停止脚本stop.sh:

#!/bin/bash APP_NAME=demo-service.jar PID=$(pgrep -f "$APP_NAME") if [ -z "$PID" ]; then echo "应用未在运行" exit 0 fi kill $PID for i in 1 2 3 4 5; do if ! kill -0 "$PID" 2>/dev/null; then echo "应用已优雅退出" exit 0 fi sleep 1 done kill -9 $PID echo "强制停止"

脚本里有两个细节值得说:一个是pgrep -f是按完整命令行匹配,避免和其他Java进程混淆;另一个是stop脚本里优雅停止等待5秒,必要时才强杀,避免误伤正常请求。

我实际操作中的体会是,部署这件事,第一次做会觉得坑特别多,但只要把流程固定下来、把命令整理成脚本,后面每次都差不多是机械操作。部署本身不难,难的是出问题时你能快速定位是哪一层出了问题。这套排查顺序我每次都在用:先看应用日志,再看端口监听,最后查防火墙和安全组。按照这个次序走下来,绝大多数问题十分钟内都能定位。

这个系列后续我还会接着写数据库接入、Nginx反向代理、HTTPS证书和开机自启。如果你按这套流程部署时卡在哪一步,大概率问题就在上面几个检查点里,先按表格自查一轮,比盲目重启更能解决问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询