☰
SpringBoot项目部署实战:从Jar包到宝塔面板+Nginx全流程
2026/10/18 18:20:47 网站建设 项目流程

部署这事儿,说简单也简单,说复杂也复杂。简单到一句java -jar app.jar就能起服务,复杂到能让你折腾一晚上还找不到问题在哪。很多刚接触 SpringBoot 的同学都有个直觉:本地 IDE 里跑得好好的,点击运行就完事儿,为什么部署到服务器上就这么费劲?宝塔面板又是干什么的?我不用行不行?

这篇文章就是想一次性把这事说透。我尽量用大白话拆解 SpringBoot 项目部署背后的原理,再结合宝塔面板讲一遍完整的实操流程,让你看完既能明白“部署到底在干什么”,也能直接跟着做把项目跑起来。

1. 部署的本质:本地运行和服务器运行的差别在哪

先说一个扎心的事实:你本地能跑,不代表服务器上能跑。这不是玄学,是两套环境之间的差异造成的。部署的本质,其实就是消除这种差异。

1.1 开发环境和服务器环境到底差在哪

你本机上的开发环境通常包括代码编辑器、Maven 仓库、JDK、MySQL 客户端、各种本地安装的中间件。IDE(比如某款常见的 Java IDE)帮你做了很多事:自动编译、自动识别依赖、内置的运行时配置、甚至帮你搞定了类路径 Classpath。你在 IDE 里点一下运行,背后是 IDE 把项目编译成了 target 目录下的 class 文件,然后启动一个 JVM 进程加载这些 class。

但服务器上没有 IDE。服务器上只有一个干净的操作系统,什么 Java 环境、MySQL、Nginx,统统都没有。你需要把项目变成一个可独立运行的东西传上去,再把这个东西交给服务器上的 Java 运行时去启动。

具体差异集中在四块:

  • 依赖差异:本地可能下载了各种依赖包,但服务器上没有 Maven 仓库,它不会自动帮你下载。
  • 配置差异:你在本地连的数据库可能是localhost:3306,密码是123456;服务器上的数据库可能是另一套密码、另一个端口。
  • 运行方式差异:本地用 IDE 跑,服务器要用命令跑,而且服务器上跑的程序不能依赖你开着电脑、开着终端进程。
  • 资源差异:本地可能 16G 内存随便造,服务器可能只有 2G,Java 启动参数没调好直接内存溢出。

提示:部署不是简单把代码“搬”过去,而是把“可运行制品 + 运行环境 + 配置”这三样东西组合起来,在服务器上复现出一个能稳定对外提供服务的进程。

1.2 SpringBoot 的可执行 Jar 包是怎么工作的

SpringBoot 项目执行mvn package之后,会在target目录下生成一个 jar 包,这个 jar 包和普通 jar 包不一样,它叫可执行 jar(fat jar)。

名字叫 fat,是因为它把项目编译出来的 class、resources 里所有配置、以及所有第三方依赖的 class 文件全部打进了这一个 jar 里。你甚至可以打开这个 jar 看看,里面有个BOOT-INF/lib目录,里面躺着你项目的所有依赖 jar。

还有一个BOOT-INF/classes目录,里面是你自己的代码。org/springframework/boot/loader里是 SpringBoot 的启动器,它负责特殊类加载逻辑——把嵌套在 fat jar 里的依赖 jar 加载到 JVM 里。

所以它能做到java -jar app.jar直接启动,本质上是 SpringBoot 自己做了一个类加载器,绕过 JDK 默认的 jar 嵌套限制。这也是 SpringBoot 部署最简单的核心原因——以前 SSM 项目要打包成 war 丢进 Tomcat 的 webapps 目录,现在内嵌 Tomcat,一个 jar 全搞定。

1.3 部署路线的演化

部署方式这几年变化很大,理解这条演化路线,你就知道为什么现在推荐用宝塔或者容器化方式了。

原始时代,大家把 war 包扔到 Tomcat webapps 下,手动启动 Tomcat,改代码就得重启。再后来有了 Jenkins,配合 Git 钩子自动拉代码、自动打包、自动重启。再后来 Docker 出现,整个应用连同 JDK 环境一起封装成镜像,一举解决环境不一致问题。再到 Kubernetes 做编排,扩缩容成了家常便饭。

对于个人项目、小公司内部系统、外包项目、毕业设计,这套容器化链路太重了。一台云服务器加宝塔面板,反而是性价比最高的路。宝塔把环境安装、站点配置、数据库管理、进程守护这些常见操作都可视化,让一个后端开发同学能在一小时内搞定整个上线流程。

2. 打包:把代码变成能跑的 Jar 包

部署的第一步不是买服务器,而是确保你能在本地打出一个干净、可用的 jar 包。很多人上来就是mvn package,打完传上去发现启动报错,再回头看发现连打包都打错了。

2.1 Maven 打包的几个关键配置

SpringBoot 项目打开pom.xml,有几个东西值得检查一下。

packaging标签确定打包方式,SpringBoot 一般用jar:

<packaging>jar</packaging>

spring-boot-maven-plugin是核心,它负责在打包时把项目依赖全部打进去,并生成启动信息。没有这个插件打出来的 jar 会缺很多东西:

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

还有一个很隐蔽的问题:如果你用了@SpringBootApplication这个入口类,官方推荐把主类放在某个包路径下,确保它能被自动扫描到。如果你把主类放错了位置,打包能过,但启动必报Unable to find main class。

2.2 打包时忽略测试和清理旧产物

很多项目的测试用例不是拿来跑服务器用的,打包时如果执意跑测试,往往因为测试环境连不上数据库直接失败。所以打包前有两个建议:

mvn clean package -DskipTests

clean是把之前的 target 删掉,避免旧 class 残留导致莫名其妙的问题。-DskipTests是跳过测试代码的编译执行。注意,这里省略了运行测试,但测试代码还是会编译。如果连测试代码编译都不想碰(有些测试代码写得不规范,编译都过不了),用:

mvn clean package -Dmaven.test.skip=true

两个命令的区别在于:前者跳过“执行”测试,后者跳过“编译+执行”测试。实战中我用第二种更多,省事。

2.3 配置文件外部化

打包之前还有一件事要想清楚:配置怎么管理。比如数据库密码、第三方密钥这类敏感信息,如果直接写在application.yml里打进 jar 包,等于把密码拱手送人。而且不同环境(开发、测试、生产)配置还不一样。

推荐的做法是:打包时只留一份通用配置,生产环境的配置放到 jar 包外面,用 SpringBoot 的外部化配置机制覆盖内部配置。启动时指定外置配置:

java -jar app.jar --spring.config.additional-location=/www/wwwroot/app/config/application.yml

这样 jar 包是同一个,不同环境通过外挂配置文件切换,上线换配置不用重新打包。这是部署中非常实用的一个小技巧。

3. 服务器环境准备:宝塔面板帮你省掉哪些事

服务器到手时是裸的,需要装各种软件环境。装环境这个事,恰恰是新手最容易绝望的地方——每装一个软件都要对着教程一步步操作,还会遇到各种依赖冲突、版本兼容问题。宝塔面板的价值就在这里:把这些事全部图形化。

3.1 安装宝塔面板

宝塔面板是一个服务器管理面板,官方提供一键安装脚本。对新手来说,只要两步:拿到服务器、执行安装命令。安装完成后,面板会输出一个带端口的管理地址和管理账号密码,浏览器访问就能进入图形化管理界面。

进入面板后第一个要做的事是安装环境套件。在面板首页的“软件商店”里,可以一键安装 Nginx、MySQL、PHP、phpMyAdmin 等。对 SpringBoot 项目来说,一般需要的是 Nginx 和 MySQL,JDK 不一定有现成一键包,如果软件商店里有 Java 相关环境工具可以用,没有的话手动装也不难。

3.2 JDK 安装的两种方式

SpringBoot 3.x 要求 JDK 17 及以上,2.x 用 JDK 8 就能跑。服务器上装 JDK 有两种主流方式。

第一种是命令行直接装 OpenJDK:

sudo apt update sudo apt install openjdk-17-jdk -y

CentOS 系列类似,只是包管理器换成了 yum,包名可能是java-17-openjdk。装完可以用java -version验证。

第二种是手动解压 JDK 压缩包,配置环境变量。这种方式的好处是版本可控,可以同时装多个 JDK,用软链接切换版本。

装完后注意一件事:确认java -version显示的版本和你项目要求的一致。很多人的坑就出在这里——项目是 JDK 17 写的,服务器默认装了 JDK 8,启动直接UnsupportedClassVersionError。

3.3 MySQL 初始化与数据库创建

宝塔装完 MySQL 后,初始密码一般写在面板首页的数据库信息区域。首次登录建议手动改掉密码,并且只允许本地访问,生产环境的数据库千万不要开外网访问——除非你想被扫库勒索。

然后在宝塔的“数据库”页面新建一个数据库,记下数据库名、用户名、密码。这些信息后面配application.yml时要一一对应。

注意:宝塔创建的数据库用户密码如果包含#、&、$这类特殊字符,写在 yml 配置里记得加双引号或单引号,不然后面解析配置时会出诡异问题。

4. 首次部署全流程实录:从上传 Jar 到成功访问

环境准备完毕,进入实战环节。这里我用一个模拟项目 X 走一遍完整流程,目标是让 jar 包在服务器上跑起来,并通过 IP 加端口访问到页面。

4.1 准备项目产物和依赖文件

本地执行:

mvn clean package -Dmaven.test.skip=true

成功后target目录下会生成一个X.jar。我习惯把它改名成更简洁的名字,比如app.jar,方便后面写脚本。同时准备好外部的application-prod.yml,里面配置了生产环境的数据源、端口等。

注意数据库的数据库名和配置对得上。常见错误是配置里写jdbc:mysql://localhost:3306/app_db,但实际建的数据库叫别的名字,启动时就会报Unknown database。

4.2 上传 jar 包到服务器

宝塔的文件管理器可以直接拖拽上传。我一般会在根目录建一个专门的部署目录,比如:

/www/wwwroot/app

把所有跟这个项目有关的东西都放里面:jar 包、配置文件、启动脚本、日志文件。目录结构清爽,后面找东西不迷路。

4.3 手动启动与测试

进入目录,直接启动:

cd /www/wwwroot/app java -jar app.jar --spring.profiles.active=prod

注意这个--spring.profiles.active=prod是把 Spring 的 profile 切换成 prod,SpringBoot 会自动读取application-prod.yml。如果配置文件名不叫这个,就得用--spring.config.additional-location指向外部配置。

第一次启动建议在前台跑,这样能直接看日志输出,方便发现问题。看到类似Started Application in 8.32 seconds的日志就说明启动成功了。

随后可以开个新终端窗口验证:

curl http://127.0.0.1:8080/api/health

能返回结果说明应用本身没问题。但注意,这个 8080 端口如果你在云服务器控制台的安全组没有放行,外部访问会直接超时。

4.4 用 nohup 让应用在后台常驻

上面那种直接启动的方式有个致命问题:一旦终端断开,进程就会被杀死,服务就挂了。真正的部署必须让进程脱离终端会话独立运行。

标准做法是 nohup:

nohup java -jar app.jar --spring.profiles.active=prod > app.log 2>&1 &

拆开看这个命令:

  • nohup表示忽略挂断信号,即使终端关闭也不退出。
  • > app.log把标准输出重定向到日志文件。
  • 2>&1把错误输出也重定向到同一日志文件。
  • 最后的&让命令在后台执行。

启动后立即查看日志:

tail -f app.log

看到成功启动的标志后,再检查进程:

ps -ef | grep java

提示:进程管理千万别用java -jar app.jar &这种简陋方式,因为一旦父进程挂了,子进程很容易变孤儿进程,服务没人管。

4.5 停止与重启的正确姿势

停止 SpringBoot 应用不能直接 kill -9 了事,那样容易丢失未写入的数据。先找 PID:

ps -ef | grep app.jar | grep -v grep

然后优雅停止:

kill -15 PID

SpringBoot 内置的优雅停机机制会处理剩余请求,再释放资源。如果等了几秒还没退,再考虑kill -9强杀。

重启就是停掉旧进程,再执行 nohup 启动命令。这一步经常被打包进脚本里,下面第 6 节会专门讲。

4.6 云服务器安全组端口放行

这里有个极易被忽略的坑。很多云服务商在服务器之外还有一层安全组或防火墙策略,不管你服务器内部怎么开放端口,安全组没放行就是连不上。

以常见的云控制台为例,需要去“安全组”或“防火墙”里添加入站规则,放行 TCP 8080 端口。宝塔面板安全页面也要确认该端口已放行。服务器内部如果开了 firewalld 或 ufw,还得单独放行:

sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent sudo firewall-cmd --reload

或者:

sudo ufw allow 8080/tcp

这么多层,漏一层就连不上。排查端口不通问题时,我一般按这个顺序:进程是否在监听(ss -lntp),宝塔是否放行,服务器防火墙是否放行,云安全组是否放行。

5. 用 Nginx 做反向代理:为什么要这么做

直接用http://服务器IP:8080访问其实已经能用了,但实际部署基本都会再装一层 Nginx 反向代理。这一步既不是多余也不是装样子,它解决几个特别实际的问题。

5.1 反向代理解决了什么问题

第一个问题:端口不体面。8080 端口既不好记也不好看,用户访问还得带着端口号。Nginx 监听标准的 80 端口(HTTP)和 443 端口(HTTPS),对外是干干净净的域名,内部再把请求转发给 8080 端口的 SpringBoot 进程。

第二个问题:HTTPS 证书管理。自己没有写 SSL 证书代码的能力,Nginx 可以轻松完成证书配置和续期。

第三个问题:日志和访问控制。Nginx 的访问日志能记录所有 HTTP 请求,还能做 IP 黑名单、限制请求速率,这些功能用 Java 代码实现既费劲又不安全。

第四个问题:负载均衡。以后项目规模大了,一个 Tomcat 扛不住,Nginx 可以把请求分发到多个后端实例,几乎是零成本升级。

用生活化类比:Nginx 是前台接待,SpringBoot 是办公室里干活的员工。访客不直接找员工,而是先到前台,由前台判断谁有空、谁能处理,然后带进去。

5.2 宝塔中配置站点

在宝塔面板里安装 Nginx 后,进入“网站”页面,添加站点。只需要填域名(没有域名可以先填服务器 IP),其他都能默认。

然后进入该站点的配置文件,把监听端口换成 80,添加关键的两段:

location / { proxy_pass http://127.0.0.1:8080; 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_pass,它把 Nginx 收到的所有请求转发到本机 8080 端口。后面那几行proxy_set_header是为了让 SpringBoot 拿到真实客户端 IP 和原始请求头。如果不设置X-Forwarded-For,后端拿到的全是 127.0.0.1,做日志分析和风控功能时就会很尴尬。

改完配置记得保存并重载 Nginx:

nginx -t nginx -s reload

nginx -t是必须的一步,配置文件有语法错误的话直接 reload 会失败,服务直接挂掉。先测试再重载是运维的铁律。

5.3 绑定域名与 SSL 证书

有域名的话,去域名服务商加一条 A 记录,指向服务器 IP。等解析生效后,浏览器访问域名就能看到应用了。

如果要用 HTTPS 访问,宝塔有免费的 Let's Encrypt 证书申请功能,在站点设置里找到“SSL”选项卡,一键申请并部署。部署完后,可以在 Nginx 配置里加一段 HTTP 自动跳转 HTTPS:

server { listen 80; server_name your-domain.com; return 301 https://$host$request_uri; }

注意:做 HTTP 跳转 HTTPS 时,301 是永久跳转,浏览器和搜索引擎都会缓存这个跳转。如果后续要改回来,记得先清浏览器缓存,否则会很煎熬。

6. 运维日常三大件:日志查看、版本升级、数据备份

部署成功只是万里长征第一步。项目上线后的日常运维,主要就三件事:看日志、发版本、备份数据。

6.1 日志文件的正确查看方式

前面 nohup 启动时把日志写到了app.log。查日志的黄金姿势:

# 实时跟踪日志输出 tail -f app.log # 查看最后 200 行 tail -n 200 app.log # 按关键字搜索最近的内容 grep -n "ERROR" app.log | tail -n 50 # 按时间范围筛选(需要日志里有时间戳) grep "2025-06-01 14:" app.log

日志文件会越来越大,时间久了把磁盘塞满。两个思路:一是直接在 SpringBoot 里配置 logback 的滚动策略,二是写个定时任务定期清理。我更推荐前者——在application-prod.yml里配置日志路径和滚动策略,按天滚动并保留 7 天或 15 天,比你写 shell 脚本处理日志靠谱得多。

SpringBoot 标配的 logback 配置片段示例:

logging: file: name: /www/wwwroot/app/logs/app.log logback: rollingpolicy: max-history: 15 max-file-size: 100MB

6.2 升级版本的流程化操作

每次发新版本,如果没有一套顺手的流程,很容易在反复停启中搞乱。我经历过几次上线事故后总结了一个稳妥的顺序:

  1. 备份当前 jar 包:cp app.jar app.jar.bak-$(date +%Y%m%d%H%M%S)
  2. 用ps -ef找到旧进程 PID,kill -15优雅停止
  3. 上传新 jar 包覆盖旧包
  4. nohup java -jar app.jar --spring.profiles.active=prod > app.log 2>&1 &启动
  5. tail -f app.log观察启动日志,确认成功
  6. 用curl测试接口,然后才让用户访问

这套流程看着简单,但“先备份再替换”这条被我刻进了骨子里。因为总有那么一次,你回滚时发现旧包已经被覆盖了,只能从 git 历史重新编译,那是真的绝望。

6.3 数据库备份与恢复

数据库是系统的钱袋子,这个钱袋子不能裸奔。宝塔面板本身有“计划任务”功能,可以创建定时任务做数据库备份。

我习惯每天早上 3 点自动备份一次,备份文件保留 7 天。宝塔的数据库备份可以手动备份,也可以设置计划任务自动备份,备份完成后会有文件生成,这样数据库出问题能恢复到前一天。

顺便说一句,服务器做了数据库备份之外,我还会额外建议你定期把备份文件下载到本地。因为服务器本身可能出现故障,只备份在同一台机器上是假安全感。

7. 部署时的常见坑:我踩过的一线实录

这些年部署踩过的坑,写出来能成一本书。挑几个大家最容易遇到的,集中讲一下排查思路。

7.1 端口被占用

启动日志报Port 8080 was already in use。这种情况十有八九是上一次启动的进程没杀掉,或者别的程序占了端口。

排查:

ss -lntp | grep 8080

会显示占用进程的 PID,然后kill -15 PID。如果 kill 不掉,查看一下是不是僵尸进程,或者用kill -9强杀。

7.2 数据库连接失败

报错通常是Communications link failure或Access denied for user。

先检查配置文件的数据库地址对不对、密码对不对;再看 MySQL 服务有没有启动(systemctl status mysqld);最后看 MySQL 是否只允许本地连接——如果权限表里用户 host 不是 localhost,你的应用也别想连上。

这里有个很常见的坑:SpringBoot 配置文件里数据库密码写错了不报语法错误,程序照样启动,但等到第一个数据库请求过来才会抛异常。所以启动成功后别急着高兴,先调一个查询接口验证一下。

7.3 内存不足启动被杀

云服务器如果是 2G 内存,跑个 MySQL 再跑个 SpringBoot 就很吃紧。

解决办法有几种思路:

  • 调整 JVM 参数限制堆内存,比如-Xms256m -Xmx512m
  • 用轻量级 MySQL 替代品或减小缓存配置
  • 升级服务器配置

调整堆内存的启动命令:

nohup java -Xms256m -Xmx512m -jar app.jar --spring.profiles.active=prod > app.log 2>&1 &

注意:-Xms和-Xmx不要设得一样大,除非你确定内存需求稳定。设一样大有触发 JVM 扩容问题的可能在极小众场景下也存在,但更关键的是设太小会有 GC 问题。我个人的经验是 512M 起步,先观察一段时间的 GC 日志和内存占用再调。

7.4 中文乱码

控制台日志中文乱码,一般是字符集不一致。JVM 默认字符集跟随系统,可以显式指定:

nohup java -Dfile.encoding=UTF-8 -jar app.jar > app.log 2>&1 &

如果接口返回的中文在数据库存下来也是乱码,那大概率是数据库表的字符集不是 utf8mb4。建库时统一指定:

CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

7.5 Nginx 502 Bad Gateway

页面报 502,意思是 Nginx 等不到后端的正常响应。最常见原因是后端进程挂了,其次是后端启动慢导致 Nginx 超时。

碰到 502 千万别慌,先看后端进程在不在(ps -ef | grep java),再看后端日志(tail -n 100 app.log)。进程挂了就看日志报什么错,进程没挂但是响应超时的话,可能是连接数不够或慢 SQL 导致线程池打满。

8. 两条提升效率的进阶技巧

下面这两个技巧不算必须,但能显著提升我个人这几个项目的运维体验,分享出来供参考。

8.1 用启动脚本替代手动敲命令

刚部署那会儿,我每次上线都是手动敲命令,敲多了想吐。后来随手写了个 shell 脚本,一键完成停止、备份、启动:

#!/bin/bash APP_NAME=app.jar APP_PATH=/www/wwwroot/app cd $APP_PATH PID=$(ps -ef | grep $APP_NAME | grep -v grep | awk '{print $2}') if [ -n "$PID" ]; then echo "Stopping old process: $PID" kill -15 $PID sleep 3 fi cp $APP_NAME $APP_NAME.bak-$(date +%Y%m%d%H%M%S) nohup java -Xms256m -Xmx512m -jar $APP_NAME --spring.profiles.active=prod > app.log 2>&1 & echo "Started. Log: app.log"

这样上线就变成了“上传 jar 包、执行脚本”两步操作。脚本本身还能做成一个“重启”入口,日常发版省了不少事。

8.2 用 JFianl 的失败重跑思路应对进程守护

对 Java 进程来说,如果因为内存溢出或未被捕获的异常直接退出,没有人去拉它它就彻底没了。生产环境最好有个守护机制。

简单的方案是把启动命令写进宝塔的计划任务,每隔几分钟检查一次进程是否存活,如果挂了就重新拉起。这算是一个不折腾人的兜底方案。如果要更专业的进程守护,可以用 systemd,配置一个 service 文件,让系统帮你管进程生命周期。但写了 systemd 之后就不太依赖宝塔窗口了,适合真正跑稳定了的项目再上。

写在最后的一点个人经验

从第一次部署手忙脚乱,到后来形成一套固定的流程,我最大的体会是:部署这件事,80% 的坑都出在环境差异上,而不是代码本身。你花在理解“每一步到底在干什么”上的时间,绝对比到处复制粘贴命令要值得。

另一个体会是:运维流程要尽可能固化。哪怕只是几行启动脚本、一个备份计划,都能让你在半夜被叫起来修 bug 时少掉一半头发。如果你完全是个新手,建议第一次部署时从头到尾手动操作一遍,别急着上脚本——手动过程中你才会真正理解进程、端口、防火墙、数据库连接这几样东西是怎么协作的。

这套流程跑通一次之后,后面的路就很顺了。等到你对 jar 包、Nginx 反代、日志排查都有感觉了,再去接触容器化也不迟,那时候你会比别人更容易理解容器到底解决了什么问题。

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

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

立即咨询