前后端分离项目写代码的人很多,真到了部署上线这一步翻车的也不少。SpringBoot + Vue 这种组合在开发环境跑得欢,一旦要搬到服务器上,数据库、端口、反向代理、跨域、路由 history 模式全部搅在一起,任何一个环节出问题,页面就是白屏、接口就是 404。我自己前后部署过几个这样的项目,踩过的坑比写业务代码时遇到的 bug 还多。这篇东西就把一套完整的宝塔面板部署方案拆开揉碎讲清楚,从服务器初始化、后端 jar 包部署、前端 Nginx 托管,到跨域转发、安全组放行、HTTPS 证书配置,全部走一遍。不管你是刚毕业的学生还是公司里被赶鸭子上架的全栈,照着我这套流程一步步来,基本能稳稳上线。
我选宝塔面板来部署,不是因为懒,而是因为它把环境安装、站点配置、证书签发、进程守护这些事集中在一个界面里,省下来的时间够你调好几个 bug。但宝塔只负责把事做简单,背后的原理你还是要懂,否则出了问题连排查方向都没有。下面我就按一次完整的上线流程来写,内容包括架构设计、环境准备、后端部署、前端部署、跨域处理、HTTPS 配置和常见故障排查,内容很长,建议收藏后对着操作。
1. 部署方案设计:为什么是宝塔 + Nginx 反向代理
1.1 前后端分离项目的部署思路
前后端分离的本质是两个独立的应用:SpringBoot 后端是一个跑在 JVM 里的 Java 进程,监听某个端口(比如 8080);Vue 前端是一堆静态文件(JS、CSS、HTML),理论上扔到任何 Web 服务器上都能跑。但“能跑”不等于“能用”,因为前端页面要请求后端接口,这就牵扯出两个问题:接口地址配置和跨域。
开发环境里,Vue 项目通过 Vite 或 Vue CLI 的 proxy 配置把 /api 开头的请求转发到后端 localhost:8080,天然不存在跨域。但打包之后,静态文件由 Nginx 托管,没有 proxy 帮你转发了,浏览器直接向服务器发请求,跨域问题立刻浮现。很多人在这里卡住,然后跑去后端的 Controller 上加一堆 @CrossOrigin 注解,或者写一个全局 CORS 配置类,虽然也能解决,但这种方案在生产环境里不是最优解。
我的方案是把宝塔的 Nginx 当作“总入口”,同时托管前端静态文件和后端反向代理。Nginx 监听 80/443 端口,用户访问域名时加载 Vue 的静态资源,当请求路径命中 /api 前缀时,Nginx 把请求转发给本机 8080 端口的 SpringBoot 进程。这样就实现了同源访问,浏览器根本感觉不到跨域的存在,前端代码里也不需要写死任何服务器 IP 或端口。
这个方案的好处很明显:前端和后端共用一个域名、一个端口,不需要把后端接口暴露到公网,减少了被扫描攻击的风险;Nginx 本身处理并发连接的能力比 Java 内嵌的 Tomcat 强,静态资源请求根本到不了后端;将来要是做负载均衡,Nginx 改几行配置就能多转发几台后端节点。
1.2 整体架构与请求流转
拿我这次部署的模拟项目 X 来说,架构非常简单,一条链路就讲清楚了:
用户浏览器 → 域名(80/443) → 宝塔 Nginx → /api 开头的请求转发到 127.0.0.1:8080(SpringBoot jar 包) ↓ / 开头的其余请求返回 Vue 静态文件(dist 目录里的 index.html)画成文字可能不够直观,我拆开说。用户的浏览器输入域名后,DNS 解析到服务器 IP,请求到达 Nginx。Nginx 先看 URL 路径:如果路径是 /js/app.js、/css/style.css 这类静态资源,直接去服务器磁盘上读文件返回;如果路径是 /api/user/login 这种带 /api 前缀的,就交给反向代理模块转发到 SpringBoot 应用。SpringBoot 处理完业务逻辑把 JSON 数据返回到 Nginx,Nginx 原样回给浏览器。整个过程浏览器只和你的域名打交道,同源策略满足,跨域问题从根上就没了。
端口规划也很重要。我的习惯是后端 SpringBoot 进程只绑定 127.0.0.1(即服务端口的 host 写成 127.0.0.1),然后用 Nginx 转发,这样公网根本访问不到 8080 端口,只有通过域名 80/443 进来的请求才能到达后端。如果你的服务器配置了宝塔防火墙和云厂商安全组,那么安全组只需要放行 80 和 443,8080 这种内部端口留在内网通信就好。这个设计后面我会反复强调,因为它真的能挡掉不少乱扫端口的恶意流量。
2. 部署前准备:服务器环境与项目打包
2.1 服务器与宝塔面板初始化
开始部署之前,先把基础环境搞定。我这次用的是一台便宜的云服务器,操作系统选了主流的 Linux 发行版,具体买多大配置根据你的项目体量来。一个简单的经验:SpringBoot 单应用 + MySQL 这种组合,2C4G 起步比较稳,别贪便宜买 1G 内存的机器,Java 进程加 MySQL 一起跑,内存很快就爆。部署前可以用命令看下系统版本和资源情况,确认没问题再继续。
服务器的环境我直接用的宝塔面板手动安装方式。这里多说一句,宝塔面板有两种安装风格:一是“编译安装”,耗时更长但性能更好,全看服务器和网络情况;二是软件商店里的一键安装。我通常选择极速安装,实际使用中两者差异不大,省下的时间更重要。进入宝塔后台之后,第一件事是在软件商店里安装 Nginx、MySQL、PHP(PHP 不一定用到,但宝塔的默认环境检测会要求装,装不装看你自己的需求),以及一个很重要的工具:进程守护管理器。Java 后台进程用它来托管,比裸 nohup 命令可靠得多。
装好环境后要处理两项安全设置:一是修改 SSH 端口和宝塔面板的默认端口,这个每个人有自己的偏好,我不展开;二是设置宝塔面板的安全组规则,确认放行了 80、443 等端口,否则后面域名访问不了,排查半天发现其实是端口没放行,就非常浪费时间了。
2.2 SpringBoot 后端项目打包
后端打包前,先检查配置文件。我在开发环境用的 application-dev.yml 里,数据库地址写的是 localhost,Redis 地址写的也是 localhost,端口 8080。生产环境如果直接套用这份配置,部署完启动后连不上数据库,又得调半天环境变量。我的习惯是创建一份生产专用的 application-prod.yml,把数据库连接、Redis、文件上传路径等全部调整成服务器上的实际值,然后在 application.yml 里激活 prod 配置。
打包命令很简单,在项目根目录执行:
mvn clean package -DskipTests跳过单元测试不是不重视质量,而是因为服务器上没有测试环境依赖的组件,很多测试用例在打包阶段根本跑不起来,白白浪费几分钟构建时间。打出来的包在 target 目录下,一般是 xxx.jar。如果你的项目用 Gradle,对应命令是 gradle bootJar。打包完成后,用 SSH 工具上传到服务器的某个目录,比如 /www/wwwroot/project-name/backend 下。我习惯把所有项目文件都放在宝塔的网站根目录管理范围内,后面配置站点时路径更清晰。
这里有个容易踩的坑:SpringBoot 内置 Tomcat 默认是打 jar 包,但有些老项目配置了<packaging>war</packaging>,这种项目部署方式完全不同,得用外置 Tomcat。部署前看一眼 pom.xml 里的 packaging 节点,别盲目照着别人的教程走。
2.3 Vue 前端项目构建
前端打包之前,检查一下环境变量文件。大部分 Vue 项目使用 Vite 构建,环境配置在 .env.production 里。我见过一个非常典型的错误:有人把后端接口地址直接写成了 http://localhost:8080/api,本地开发验证没问题,打包上传后手机访问页面,所有接口请求全打到了手机自己的 localhost,结果页面能开但数据全空。
生产环境我推荐根本不去配置 VITE_API_BASE_URL,而是让前端代码统一用相对路径 /api 开头。这样做的好处是:代码部署在哪个域名下都不需要重新构建,Nginx 负责把 /api 转发到正确的后端地址。如果你的项目里用到了各种二次封装过的 axios 实例,把 baseURL 改成 '/api',然后重新 npm run build 生成前端产物。构建产物是 dist 目录,里面是 index.html 和一堆静态资源。
打包命令:
npm install npm run build如果 npm install 特别慢,可以把 npm 源切换到国内镜像,速度会提升非常多。构建完成后,可以在本地用 nginx 或者随便一个静态服务器工具预览一下 dist 目录,确认页面能正常打开、路由跳转没问题,再上传到服务器。
3. 后端部署实操:数据库、配置与进程管理
3.1 数据库准备与数据导入
后端部署的第一步是先搞数据库。进入宝塔面板的数据库管理页面,新建一个数据库,名字建议和项目名一致或相近,字符集选择 utf8mb4,排序规则选 utf8mb4_general_ci 或 utf8mb4_unicode_ci 都行。utf8mb4 能存 emoji 表情和一些生僻字,现在新项目基本都默认用它。
数据导入有两种方式:如果项目自带 SQL 初始化文件(一般是 schema.sql、data.sql 这类),在宝塔数据库管理页面点击“导入”,选择 SQL 文件上传执行即可。如果项目用的是 Flyway 或 Liquibase 这种数据库版本管理工具,启动后它会自动执行迁移脚本,不需要手动导入。还有一种情况是项目原本在本地开发,数据库里有测试数据,想把本地数据同步到服务器,可以用 Navicat 这类工具远程导出一份 SQL,再到服务器上导入。注意导入前检查 SQL 文件里的建库语句,如果包含 CREATE DATABASE,执行时可能会因为库已存在而报错,最稳妥的做法是把 SQL 文件里的 CREATE DATABASE 和 USE 语句去掉,只留表结构和数据。
数据库导入完成后,创建一个专用的数据库账号也可以直接在面板里生成,密码设置复杂一点。生产环境的密码,千万别用 root 账号直接连业务库,万一代码里把 SQL 注入漏洞放出来了,拿到 root 权限就全完了。单独授权一个账号,只给这个业务库的增删改查权限,够用就好。
3.2 后端配置文件调整与启动
上传 jar 包到服务器后,还要同步处理配置文件。我遇到过一个很折磨的场景:项目代码是从某代码仓库拉下来后打包的,本地调试和服务器上的数据库密码、Redis 密码都不一致,但 jar 包里的配置文件已经被打包进去了,改代码再重新打包很费时间。后来我的方案是:在 jar 包同级目录放一个 application-prod.yml,启动命令通过 --spring.config.location 参数指定外部配置文件,这样改配置不需要动 jar 包。
启动命令(示例):
cd /www/wwwroot/project-name/backend nohup java -jar -Xms512m -Xmx1024m my-project.jar --spring.profiles.active=prod > my-project.log 2>&1 &逐步解释一下:-Xms512m -Xmx1024m 是设置 JVM 初始堆和最大堆内存,根据你的服务器内存来调整,别把最大堆设成超过服务器物理内存,否则系统直接卡死;nohup 是让进程在后台持续运行,不随终端关闭而退出;> my-project.log 2>&1 是把标准输出和标准错误都写到日志文件里,排查问题就靠这份日志;--spring.profiles.active=prod 指定生产环境配置。
启动后验证是否成功,用下面几个命令:
# 查看进程是否存活 ps -ef | grep java # 查看端口是否监听 netstat -tlnp | grep 8080 # 实时跟踪日志输出 tail -f my-project.log如果日志里出现 Started Application in XX seconds 这类字样,说明启动成功。要是日志报错,比如数据库连接失败、端口被占用,照着报错信息改配置重启即可。第一次启动时我用 tail -f 盯着日志,能看到完整的启动过程,比盲目等几秒钟再试接口要高效得多。
3.3 使用进程守护管理器设置开机自启
裸的 nohup 启动有一个问题:进程会因为各种原因退出,比如内存不够被系统杀掉、JVM 自身崩掉、服务器重启后进程消失。手动重新启动倒不难,但生产环境不能总指望人守在那里。宝塔面板软件商店里的进程守护管理器,配一个守护任务就行。
配置要点:运行目录填 jar 包所在目录,启动命令填 java -jar my-project.jar --spring.profiles.active=prod,日志文件填你希望输出日志的路径。勾选开机自启后,重启服务器也能自动拉起进程。如果你用的系统里有 systemd,也可以手写一个 service 文件,效果一样,但宝塔面板操作起来更直观,对不熟悉 systemd 文件的开发者会更友好。
进程守护管理器还有一个功能叫“异常退出自动重启”,这个必须打开。Java 进程有时候因为突发情况挂掉,守护进程检测到后会在几秒内重新拉起,别人正在用的系统就不会受到长时间中断的影响。我实际使用中发现,设置里跟内存相关的参数要谨慎调整,别盲目把占用上限调大,否则进程守护本身可能会和你的 Java 应用抢内存。
4. 前端部署与跨域处理:Nginx 站点配置详解
4.1 上传前端文件与创建站点
先把 vue 构建好的 dist 目录整体上传到服务器。路径我建议放在 /www/wwwroot/project-name/dist 下,注意是 dist 目录里面的文件直接放在这个路径,而不是 dist 目录再套一层。然后到宝塔面板的“网站”页面添加站点,域名填你的实际域名,如果还没有域名,可以先填服务器 IP,后面有域名了再修改。根目录指向 dist 目录,其他选项默认。
添加完站点之后,默认配置基本是能访问静态页面的。但这里有一个关键点:Vue 项目如果使用 history 路由模式,直接访问非首页路由(比如 /login、/user/profile)会出现 404,因为 Nginx 会去找磁盘上对应的物理路径,而 SPA 里只有 index.html。解决办法是配置 try_files,让所有非静态资源的请求都回退到 index.html:
location / { root /www/wwwroot/project-name/dist; index index.html; try_files $uri $uri/ /index.html; }$uri 是请求的路径,如果磁盘上找不到对应的文件,就返回 $uri/ 目录下的 index(通常也找不到),最后回退到 /index.html。这样 Vue Router 拿到 index.html 后就能根据 URL 自己判断渲染哪个页面,前端路由不会再 404。
4.2 跨域问题的根因与 Nginx 反向代理配置
前端能打开页面了,但接口请求还是 404,这是前后端分离项目部署时最普遍的问题。原因很简单:Nginx 只拿到了静态文件,不知道 /api 请求应该转给谁。我在 1.2 节梳理链路时说过,Nginx 必须配置反向代理,把 /api 开头的请求转发给 SpringBoot 进程。在 Nginx 配置文件里的 server 块中加一个 location:
location /api/ { 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_pass 后面的 URL 有没有带末尾斜杠。如果写成 proxy_pass http://127.0.0.1:8080;,那么请求 /api/user/login 会原样转发到后端,变为 /api/user/login;如果写成 http://127.0.0.1:8080/,则会把 /api/ 前缀去掉,转发为 /user/login。这两种情况后端接口地址不同,选哪种取决于你的后端 Controller 里有没有加 /api 前缀。我的项目里后端接口统一使用 /api 前缀,所以 proxy_pass 不带末尾斜杠,保证路径原样透传。这是最容易踩坑的地方,一定要检查。
另外三行 proxy_set_header 相信你已经见过很多次,解释一下:Host 保持浏览器发起请求时的域名,Java 后端某些代码里用 request.getServerName() 获取当前域名来拼接绝对 URL 时,没有这行拿到的就是 127.0.0.1:8080;X-Real-IP 和 X-Forwarded-For 是为了让后端能记录真实客户端 IP,不然日志里看到的全是 127.0.0.1。
配置完成后,每个 location 块都是独立的,你需要注意 server 块里到底允不允许重叠匹配。实际操作中,把静态文件的 location / 放在前面,把 api 的 location /api/ 放在它后面,Nginx 会按最长前缀匹配选择规则,不会冲突。保存配置后执行 nginx -t 检查语法,然后平滑重载:
nginx -t nginx -s reload重载完成后再访问页面,打开浏览器开发者工具,Network 面板里看接口请求的状态码:如果请求从红色 404 变成 200,说明反向代理通了;如果变成 502,大概率是后端进程没起来或者端口不对;如果是 504,可能是后端处理超时,需要检查后端日志和数据库连接池配置。
5. 安全组、域名与 HTTPS 配置
5.1 云厂商安全组与端口放行
很多新手在本地调试时一切正常,一到服务器上死活访问不了,最容易被忽略的就是云厂商的安全组。宝塔面板自带防火墙只是服务器内部的一层拦截,而云厂商控制台的安全组相当于机房门口的保安,两层都要放行才能放流量进来。
以我这次部署为例,我在云厂商控制台的安全组规则里放行了这些端口:80 和 443(Web 服务)、SSH 端口(用于远程连接)。后端 8080 端口不要放行,因为开放在公网上没有任何意义,反而多一个攻击面。如果一开始没有域名只用 IP 测试,只放行 80 也够,前端通过 Nginx 转发到内网接口,不需要公网直接访问 8080。
如果你在服务器上用的是宝塔防火墙,还要在宝塔安全页面里同样放行 80 和 443。两边都检查完了,再访问域名测试,如果还是打不开,用本地 telnet 命令检查端口通不通,比如 telnet 你的服务器IP 80。端口不通时优先查安全组和云平台那边的状态。
5.2 域名解析到服务器
有域名的话强烈建议直接绑域名,比用 IP 访问体验好太多。进入你的域名服务商控制台,添加一条 A 记录,主机记录按需填 www 或 @,记录值填服务器 IP,TTL 默认就好。解析生效一般几分钟到一小时内,可以在本机用 ping 命令看域名是否解析到了新 IP。
解析生效后,回到宝塔的站点设置里把域名填进去,一个站点可以绑定多个域名,比如 example.com 和 www.example.com,可以都绑到同一站点上。此时通过域名访问页面,Nginx 会使用你配置的 server_name 匹配站点,未来你甚至可以在同一台服务器上部署多个项目,不同域名走不同的 server 块,非常方便。
记得把 4.2 节的 Nginx 配置放到域名对应的站点配置文件中,而不是随便建一个.conf 文件。检查方式:确认 /www/server/panel/vhost/nginx/ 里站点配置文件的 server_name 与你的域名匹配。可以每步都用 curl 测试:
curl -I http://你的域名返回 200 说明站点配置生效,返回 404 则检查路径,返回 403 则可能是目录权限或者 index 配置问题。
5.3 申请 SSL 证书并启用 HTTPS
现在的项目不上 HTTPS 基本说不过去。宝塔面板的 SSL 证书申请做得很省心:在站点设置里打开 SSL 选项卡,选择 Let's Encrypt,勾选你绑定的域名,点击申请即可。如果之前域名解析没有生效,申请会失败,所以先确保前面步骤完成了再走这一步。申请成功后,宝塔会自动把证书文件放到对应目录并配置好站点。
配置 HTTPS 后还需要设置强制跳转:宝塔的 SSL 设置里直接有“强制 HTTPS”开关,开启后访问 http 的请求会 301 跳到 https。这个很重要,否则用户输入域名默认走 80 端口,页面不够安全。
部署完成之后,在浏览器地址栏访问你的域名,看到锁标志就说明证书生效了。顺便检查一下页面里有没有混入 http 请求的静态资源,有的浏览器拦截混合内容,可能导致某些图片或脚本加载不了。用浏览器开发者工具的 Console 看有没有 Mixed Content 报错,如果报错,去前端代码里把资源地址改成 https 或使用相对路径,重新构建后部署。
6. 部署完成后:性能优化与常见故障排查
6.1 常见问题速查表
部署这件事,百分之八十的时间都花在排错上。我把这些年遇到的高频问题整理成一张表,你遇到类似情况可以直接对号入座。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 首页打不开,一直转圈 | 防火墙未放行 80/443;Nginx 没启动;域名解析未生效 | 检查宝塔安全与云厂商安全组;systemctl status nginx;ping 域名确认解析 |
| 页面能开但接口 404 | 反向代理 location 没配置;proxy_pass 路径前缀不对 | 检查 Nginx 配置文件 /api/ 段;确认 proxy_pass 是否带斜杠 |
| 接口 502 Bad Gateway | 后端 jar 包没启动成功;后端端口与 proxy_pass 不一致 | 检查 java 进程、netstat 端口、手动 curl localhost:8080 测试 |
| 接口 504 Gateway Timeout | 后端接口执行慢;数据库连接池或 SQL 慢查询 | 看后端日志;检查数据库连接池配置;优化慢 SQL |
| 数据库连接失败 | 数据库账号密码错误;mysql 服务未启动;端口未放行 | 检查数据库配置;systemctl status mysqld;远程连一下数据库 |
| Vue 路由被访问时 404 | SPA 路由模式缺少 try_files 配置 | 在 location / 中配置 try_files $uri $uri/ /index.html |
| 页面能开但图片加载不了 | 静态资源路径写错;磁盘权限不足 | 检查 Nginx 静态路径与实际文件位置;chmod 755 目录 |
| jar 包启动后自动退出 | 端口被占;内存不足;配置文件加载错误 | 检查 8080 端口占用;看启动日志和进程守护日志;调整 JVM 堆大小 |
| HTTPS 证书申请失败 | 域名解析没生效;80 端口不通 | 确认解析和端口,等待生效后重新申请 |
| 修改 Nginx 配置后报错 | 语法错误;缺少分号或括号 | nginx -t 检查语法,定位问题行号 |
这张表不是万能药,但确实能覆盖我遇到过的绝大部分问题。遇到报错时先别慌,按“看日志 → 查端口 → 测链路”的顺序排查,比盲目改配置高效十倍。举一个真实场景:接口 502,很多人直接去看后端代码,其实第一步应该 curl http://127.0.0.1:8080 看后端是否在监听,如果连接被拒绝说明进程没起来,再去翻日志,根本不用碰代码。
6.2 内存、日志与日常维护注意事项
Java 应用部署完不等于万事大吉,运行期的维护更重要。内存这块,如果你的服务器只有 2G 内存,同时跑 MySQL、Nginx、Redis、SpringBoot,很容易触顶。我建议给 JVM 堆设置上限,比如 -Xmx512m,让出一些空间给数据库和系统本身。同时把 MySQL 的性能配置调低一点,宝塔面板里有 MySQL 性能调整选项,选择小内存服务器模式,避免 MySQL 占用过多内存导致 OOM。
日志管理也要做好。nohup 启动方式下日志文件会越来越大,几个月不管可能塞满磁盘。建议每周检查一下日志目录,或者用 logrotate 做日志轮转。宝塔的后端项目日志也可以配在进程守护管理器里,开启“写入运行日志”,但依然存在日志膨胀问题。简单的做法是写一个 crontab 定时任务,每天清理一下超过一定大小的日志文件,或者按天分割日志。很多人会忽略磁盘空间,直到某天站点打不开了,SSH 进去一看磁盘 100%,才发现是日志把磁盘塞满了。
日常维护还少不了备份。宝塔的“计划任务”功能可以定时备份数据库到本地或云存储,SQL 自动压缩后上传到其他地方更安全。我的习惯是每天凌晨备份一次数据库,保留最近 7 天。代码本身有版本管理工具,但数据库没有,出问题时一份可用的备份比什么都值钱。
6.3 先写到这里,分享几个让我少走弯路的习惯
部署完成之后再用一段时间,会有一些自己的切身体会。我最后分享几个纯经验层面的东西,都是实操后才理解到的,希望能帮你少折腾几轮。
第一,严格分离环境配置。开发环境和生产环境的数据库、Redis、文件路径一定不要混用,准备独立的 prod 配置文件,从源头减少跨环境带来的低级故障。第二,后端端口不要暴露公网。只要用了 Nginx 反向代理,后端服务端口只需要监听 127.0.0.1,加上安全组不放行,双重保险。第三,动手改 Nginx 配置前先备份原文件,用 nginx -t 测过语法再 reload。改坏配置导致全员无法访问的教训,经历过一次就足够长记性了。第四,记录每次部署的操作过程和遇到的问题,哪怕只是简单记在笔记软件里。一段时间后这些记录会成为你排障时最高效的参照。
前后端分离项目的部署不难,难的是一步一步把每个细节确认到位。按照我这套流程走一遍,你会发现自己也能用一块干净的服务器面板,把一个前后端分离项目从代码变成线上服务。实话说,成功上线的感觉还挺有成就感的。下一次再部署类似项目,流程熟练之后基本半小时左右就能完事,熟练度和自动化程度会越来越高。