1. 本地跑通前后端项目:两个端口和一个代理的联调基本功
我一直觉得,前后端分离项目最折腾人的不是写代码,而是把它从“我电脑上能跑”变成“别人也能访问”。本地上有Vite热更新,后端有Spring Boot一键启动,一切都很美好;真到部署那天你会发现,能跑的代码在服务器上不一定能跑,能跑的程序公网不一定能访问。这篇文章就用一个典型的Spring Boot + Vue前后端分离项目打样,把从本地联调、打包构建、服务器部署到公网访问的整条链路拆开讲一遍。适合刚接触部署的新手,也适合已经部署过一两次但总在某些环节卡壳的人。
1.1 本地开发环境的“最小可用配置”
前后端分离项目在本地跑通,看起来是小事,其实是后面所有部署动作的基准线。你连本地都跑不通,服务器上更别想一次成功。
先说后端。Spring Boot项目本地启动基本没什么坑,JDK版本对上、Maven依赖拉下来,mvn spring-boot:run或者IDE里直接跑main方法就行。但有一类项目需要注意——如果你拿的是若依、mall这类开源脚手架,启动前务必检查application-druid.yml里的数据库连接,以及Redis地址。很多人启动报错不是代码问题,是本地MySQL没起、Redis没起,或者密码不对。这类项目往往还依赖一个初始化SQL脚本,你导入数据库了吗?没导的话启动时表不存在,照样白屏。
再说前端。Vue项目clone下来之后,npm install是第一步。这里有个小建议:别直接npm install,先看package.json里用的包管理器是npm还是yarn还是pnpm。若依老版本用npm,新版本用pnpm,混用容易产生锁文件冲突。装完依赖后npm run dev,默认端口一般会落在5173或80。注意看控制台输出的实际地址,因为如果80被占用,Vite会往后顺延一个端口,这时候你访问的就不是你以为的那个地址。
前后端都启动后,验证联调是否正常的标准动作是:打开浏览器F12,切到Network,找一个登录接口或者列表接口,看请求是否成功返回JSON。如果返回401、404、500,按下面的顺序查:后端控制台有没有打印异常、接口路径是否匹配、有没有走代理转发。
1.2 联调阶段最容易翻车的“跨域”问题
前后端分离之后,前端跑在5173端口,后端跑在8080端口,两个端口不同,浏览器就会拦截前端的AJAX请求——这就是跨域。解决跨域有三种主流姿势,我建议按优先级排序:
- 前端开代理(最常用):在Vite或webpack的配置里加一个
proxy,把/api开头的请求转发到http://localhost:8080。这样浏览器看到的请求是同源的,根本不触发跨域。 - 后端开启CORS:Spring Boot里写一个
WebMvcConfigurer,配置允许跨域的域名、请求头和请求方法。适合前端无法改代理的场景,比如别人在局域网里直接访问你的前端地址。 - 浏览器关跨域校验:仅限临时调试,千万别在正经项目里干这事。
实际使用中我推荐前端代理方案,因为它的转发逻辑跟生产环境的Nginx反向代理天然一致。你在本地配的/api -> 8080,到了生产环境配的就是/api -> 服务器后端端口,一个思路用到底,不需要来回切换思维。Vite的配置长这样:
// vite.config.js server: { host: '0.0.0.0', port: 80, proxy: { '/prod-api': { target: 'http://localhost:8080', changeOrigin: true } } }注意这里有个细节:changeOrigin一定要设成true。不设的话,后端可能会拿到前端的Host头,某些框架对Host头有校验(比如Spring Security在某些配置下),会直接拒绝请求。这个坑我见过不止一次。
2. 打包构建阶段最容易被忽略的三件事:环境配置、产物目录与资源路径
本地跑通只是万里长征第一步。接下来要做的,是把前后端源码变成可以在服务器上执行的产物。这一步看似简单,实际是整个部署过程中翻车率最高的区域之一。我总结了三件最容易忽略的事。
2.1 后端:Maven打包前必须确认的配置项
Spring Boot项目的后端打包命令很简单:
mvn clean package -DskipTests命令行敲下去,等一两分钟,target目录下就会出现一个xxx.jar。但很多人的问题恰恰出在“太简单”上——包打出来,一放到服务器上就起不来,或者起来了但接口报错。
先检查打包用了哪套配置。Spring Boot项目一般有application.yml、application-dev.yml、application-prod.yml这种按环境拆分的配置。启动时通过--spring.profiles.active=prod来指定。很多人开发时一直用dev配置,数据库连的是本地,Redis连的是本地,打包时也没改,传到服务器上之后,代码起来了但连不上数据库,查半天才发现配置不对。
所以打包前你要确认三样东西:生产数据库地址、Redis地址、文件上传路径。这三样至少先按服务器实际情况改好,或者用环境变量占位。比如在application-prod.yml里写:
spring: datasource: url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:3306/yourdb?useUnicode=true&characterEncoding=utf8 username: ${MYSQL_USER:root} password: ${MYSQL_PASSWORD:root}这样在服务器上启动时,通过环境变量注入,就不用每次重新打包了。
还有一个容易被忽略的点:mvn clean package时会执行测试类,如果某个测试用例失败,打包会中断。加上-DskipTests跳过测试没问题,但要提醒自己:这不是让你永远不跑测试,只是部署场景下的取舍。
顺带说一下IDEA部署项目时添加Deployment没有Artifact这个老问题。如果你还在用SSM那套、需要打war包扔进外部Tomcat的老古董方案,IDEA里找不到Artifact多半是因为你的项目没有创建Web Facet,或者没有执行构建。现在的Spring Boot项目直接打jar包、用内嵌Tomcat,根本没这个烦恼。如果你确实要打war包,请在pom.xml里把packaging改成war,然后在启动类上继承SpringBootServletInitializer并重写configure方法。
2.2 前端:构建产物的路径陷阱
前端打包命令通常是:
npm run build产物在dist目录下,里面有index.html、static目录、各种js/css文件。看起来一切正常,但传到服务器上用Nginx一配,完蛋了——首页能打开,但点登录按钮之后,接口全部404,或者刷新页面变成404。
这两个问题的根子都在“路径”上。
第一个问题,接口404。打开dist/js下某个js文件,搜索http://,如果发现代码里写死了http://localhost:8080/api,那基本可以断定你忘记改环境变量了。前端项目里一般有个.env.development和.env.production文件,生产构建时要用/prod-api这种相对路径,让请求走Nginx的代理,而不是写死IP。一旦写死了localhost,浏览器就是拿用户自己的电脑去访问8080端口,能通才怪。
第二个问题,刷新404。这是因为你的前端路由用了history模式,刷新页面时浏览器会向服务器请求当前路径,比如http://你的域名/sys/user,但服务器上根本没有/sys/user这个物理文件,Nginx就返回404了。解决办法是让Nginx把所有非静态文件的请求都重写到index.html,由前端路由自己接管。这个配置我会在下一章给出。
2.3 前端构建还有一个不起眼的坑:publicPath与资源加载
如果你的项目部署在域名根路径下,publicPath用默认的/就行。但如果你想让前端和后端同时跑在一个IP的不同端口,或者放在Nginx的某个子路径下(比如http://IP/xxx/),那就必须设置publicPath为对应子路径,否则js/css全部加载不出来,因为浏览器会去根路径找资源。
这事我在部署多个Web项目到同一台Nginx时吃过亏。两个前端项目,一个放根路径,一个放在/mall路径下,第二个项目的publicPath如果不设为/mall/,页面就是纯白。Vite里改base配置,Vue CLI里改publicPath。
3. 服务器部署布局:Java进程负责API,Nginx负责静态资源与反向代理
本地跑通了,包也打好了,接下来就是真正的部署环节。一台干净的云服务器拿到手,需要装JDK、装Nginx、传文件、起服务、配代理。这一章节我把整条链路走一遍。
3.1 后端Jar包的常规启动姿势与日志处理
假设你有一台Linux服务器,IP是1.2.3.4(从你的云服务商控制台可以查到公网IP)。先把jar包传到服务器上,传送方式很多:scp命令、宝塔面板、rz命令都行。我个人习惯用scp,一条命令搞定:
scp target/your-app.jar root@1.2.3.4:/opt/app/然后在服务器上确认Java环境。Spring Boot项目建议用JDK 8或11,具体版本看你的pom.xml里java.version。java -version看一下,没有就装,有但版本不对也得调整。
启动Java进程的标准命令是:
nohup java -jar /opt/app/your-app.jar --spring.profiles.active=prod > /opt/app/logs/app.log 2>&1 &这个命令的意思是:后台启动jar包,使用prod环境配置,日志输出到/opt/app/logs/app.log。加nohup是为了防止你关闭SSH终端时进程被一起带走。&是让程序在后台运行。2>&1是把错误输出也重定向到同一个日志文件,这样排查问题只看一个文件就行。
等几秒之后,怎么确认启动成功?tail -f /opt/app/logs/app.log看日志,看到Started Application in xx seconds就是成功了。这时候你可以先在本机(服务器上)验证一下接口是否通:
curl http://127.0.0.1:8080/api/test如果curl有返回,说明后端程序本身没问题。如果卡住或者返回空,先别急着查外部,90%的情况是数据库连不上、Redis连不上、或者端口被防火墙挡了。
这里要提一个关于日志的细节。Spring Boot默认用Logback打日志,如果你不额外配置,日志文件会无限增长,或者按天滚动但保留所有历史。热搜词里有一条“Spring Boot项目部署后日志只保留三个月”,就是典型的运维需求。在logback-spring.xml里配maxHistory就行:
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>/opt/app/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>90</maxHistory> </rollingPolicy>配上之后,日志自动保留90天,超过的自动删除,磁盘不会爆。
3.2 Nginx配置:前端静态目录与后端接口代理的完整模板
后端起来了,接下来处理前端。把dist目录传到服务器上,比如放到/opt/nginx/html/mall,然后编辑Nginx的配置文件。下面是我经常用的一套模板,直接抄就能用:
server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /opt/nginx/html/mall; index index.html; try_files $uri $uri/ /index.html; # 解决history路由刷新404 } # 后端API反向代理 location /prod-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; } # 静态资源缓存(可选) location /static/ { root /opt/nginx/html/mall; expires 7d; } }配置完执行nginx -t检查语法,然后nginx -s reload让配置生效。
这里有两个点值得单独拿出来讲。
第一,try_files $uri $uri/ /index.html;这行是解决history模式路由刷新404的关键。它的含义是:如果请求的URI找不到对应文件,就返回index.html,让前端路由去解析。没有这行,你访问/sys/user刷新就404。
第二,proxy_pass http://127.0.0.1:8080/;最后这个斜杠有讲究。如果proxy_pass带了路径(比如http://127.0.0.1:8080/),那么Nginx会把location匹配到的部分替换掉。如果请求/prod-api/login,实际转发到后端是/login,因为/prod-api被吃掉了。如果你的后端接口本身也有/prod-api前缀(比如后端Controller的RequestMapping里带着这段),那proxy_pass就不能带斜杠,写成http://127.0.0.1:8080,这样请求会原样转给后端。这一正一反,错一个就404。
3.3 若依这类脚手架项目的特殊部署姿势
拿若依前后端分离框架举例。若依前端的请求路径默认带/prod-api,后端接口的上下文是/prod-api。按上面那套配置,Nginx的location /prod-api/和proxy_pass http://127.0.0.1:8080/正好匹配——前端发的/prod-api/login,Nginx去掉了/prod-api,转给后端/login;后端返回数据,前端完美拿到。这是若依官方推荐的部署方式。
但是热词里还有一条“若依前后端分离框架部署”,很多人用的是另外一套方式:前端和后端分开跑在服务器上,前端直接改nginx配置把/prod-api指到服务器公网IP:8080。这种做法也能通,但有个前提——后端必须开启CORS,否则跨域问题会让浏览器把请求拦截掉。我的建议是:统一走Nginx反向代理,让浏览器始终只访问同一个域名,不要在dist里写死后端地址,也不要在前端代码里配http://服务器IP:8080。一个入口解决所有问题,出问题也只需查一层代理。
3.4 Tomcat部署Web项目的遗留场景:war包与外置Tomcat
现在主流是Spring Boot内嵌Tomcat,但还是有不少老项目、课程项目走的是“外部Tomcat部署war包”的路线,热搜词里“tomcat部署web项目”就是这个场景。
如果你拿到的是war包,部署方式是丢到Tomcat的webapps目录下,启动Tomcat后自动解压。JDK版本要和Tomcat兼容,Tomcat 8.5以上建议JDK8+。部署后访问路径要带项目名,比如http://IP:8080/your-project-name/,如果想去掉项目名,可以把war包改名为ROOT.war,它会自动落到根路径。IDEA里跑Tomcat部署时出现的“添加Deployment没有Artifact”,多半是你没有先执行一次构建,或者Web Facet没配置好。先Build一下项目,确保target下有war包,再打开Deployment面板,Artifact才会出现。
4. 从IP到域名的公网访问链路:解析、端口、安全组和备案缺一不可
现在,你的后端Java进程在服务器上跑着,Nginx也配好了,理论上浏览器已经可以通过http://服务器公网IP访问到你的前端页面。但如果实际测试发现打不开,大概率是卡在下面这一条链路上:公网IP、域名解析、安全组放行、服务器内部防火墙。
4.1 服务器有公网IP,但外网访问不了?先查三层
我在排查“知道了服务器的公网怎么访问”这类问题时,永远按三层来查:
第一层,云服务商控制台的安全组。这是最容易被忽略的。大多数云厂商默认只在安全组里放行了22端口(SSH),而80、443端口没有对外开放。你在服务器上用curl http://127.0.0.1:80能通,但外网就是访问不了,原因就在这。需要登录云服务商控制台,找到你的实例,进入安全组规则,添加放行规则:协议TCP,端口80,来源0.0.0.0/0。443要不要放,取决于你是否配了HTTPS证书。
第二层,服务器内部防火墙。检查firewalld或iptables的状态。CentOS系统常见的是firewalld:
firewall-cmd --state firewall-cmd --permanent --add-port=80/tcp firewall-cmd --reload如果装了宝塔面板,宝塔的安全入口也要放行端口,不然端口在面板里被拦截,照样访问不了。
第三层,Nginx有没有监听正确端口。ss -lntp看一下,确认Nginx确实在监听80端口。有时候Nginx没启动,或者启动失败(配置语法错误),外网自然访问不了。
这三层检查完,http://公网IP应该能打开你的前端页面了。
4.2 域名绑定后面的几个细节
用IP访问没问题,接下来很多人会去注册一个域名,然后做A记录解析,把域名指向服务器IP。域名解析的操作很直观——在云厂商的DNS控制台添加一条A记录,主机记录填www或者@,记录值填你的服务器公网IP。
但域名解析生效需要一点时间,而且不同网络环境生效速度不一样。本地刷新一下DNS缓存(ipconfig/flushdns),然后用ping或者在线工具解析一下域名,确认指向是否已经生效。
如果域名解析配置了但一直不生效,看看服务器备案状态。按照国内平台的合规要求,域名如果绑定到大陆区域的服务器并通过80/443端口提供Web服务,是需要在接入服务商的平台完成备案流程的,否则域名会被阻断访问。如果你不想等这个流程,临时绕过的方式是用IP直连,或者把服务部署在非80端口(比如8080、8888),但这只适合测试环境,正经上线还是得走完合规流程。这一点务必重视,很多人域名解析了一天都访问不了,最后发现是备案没完成。
4.3 HTTPS:从“能用”到“好用”的升级
HTTP能访问之后,我强烈建议顺手把HTTPS配上。原因很实际:如果你的项目涉及登录、支付这类敏感操作,HTTP明文传输等于把用户的账号密码裸奔在网络上;而且现在主流浏览器对HTTP站点会有“不安全”提示,非常掉档次。
配HTTPS的流程是:获取SSL证书(有免费的一年期DV证书),然后把证书文件放到服务器上,修改Nginx配置:
server { listen 443 ssl; server_name your-domain.com; ssl_certificate /usr/local/nginx/cert/your-domain.pem; ssl_certificate_key /usr/local/nginx/cert/your-domain.key; # 与HTTP配置相同的 location 规则 location / { root /opt/nginx/html/mall; index index.html; try_files $uri $uri/ /index.html; } location /prod-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; } }配好后,最好把HTTP的80端口也保留,做一个301跳转到HTTPS,避免用户访问旧地址时404:
server { listen 80; server_name your-domain.com; return 301 https://$host$request_uri; }我经历过一个线上事故:前端用了HTTPS,后端接口也走了Nginx代理,但因为后端代码里获取请求协议时用错了方式,导致某些回调地址还是HTTP,登录回调时无限循环。后来统一在Nginx层加了X-Forwarded-Proto头,并在Spring Boot里配置server.forward-headers-strategy,问题才解决。
5. 手动部署之外的更好选择:用Docker把部署变成流水线
手动传到服务器上、nohup起Java进程、手动改Nginx配置,这套流程跑通一次之后,你会开始觉得烦——每次发版都要重复一遍,还容易漏步骤。这时候就该上Docker了。Docker的价值不仅在于部署,更在于让“本地能跑”和“服务器能跑”之间的差距趋近于零。
5.1 后端容器化:从Jar包到镜像
一个Spring Boot项目的Dockerfile非常简洁:
FROM openjdk:8-jre-alpine WORKDIR /app COPY your-app.jar /app/app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar", "--spring.profiles.active=prod"]然后构建镜像、启动容器:
docker build -t your-app:1.0 . docker run -d --name your-app -p 8080:8080 \ -e MYSQL_HOST=127.0.0.1 \ -e MYSQL_PASSWORD=yourpass \ -v /opt/app/logs:/app/logs \ your-app:1.0这里用-e传入环境变量,对应我们在配置文件里留的占位符。-v把容器内的日志目录挂载到宿主机,不然日志写在容器里,容器一删就没了。
用Docker跑项目有个隐性好处:宿主机上不用装JDK了。以前换一台服务器,要装JDK、配环境变量,环境不对项目起不来;现在镜像里自带JDK,到哪都能跑。这点对很多被“JDK版本不一致”坑过的人来说,简直是救星。
5.2 前端容器化:用Nginx镜像跑静态资源
前端的Dockerfile也简单,基于Nginx镜像,把构建好的dist目录拷贝进去:
FROM nginx:alpine COPY dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80启动:
docker run -d --name web -p 80:80 your-web:1.0注意前端容器需要和后端容器通信,最简单的做法是用docker-compose编排,把前端、后端、数据库、Redis一起管起来。之前有一段时间流行“Docker部署若依项目”,本质就是写一个docker-compose.yml,把若依的后端、前端UI、MySQL、Redis全部定义好,一条docker-compose up -d全部拉起来。这也是目前最推荐的方式,复现成本极低。
5.3 Docker部署需要留意的日志与数据卷问题
用Docker部署后,日志和数据的持久化是必踩的坑。容器本身是无状态的,一删重建,里面的数据就没了。数据库容器一定得挂数据卷,否则跑了几天的数据,一次docker-compose down直接清空,哭都来不及。日志同理,尽量挂到宿主机目录,方便用tail、grep排查问题。
此外,如果直接用--restart=always让容器自动重启,要确认你的应用启动逻辑是否幂等——启动时是否重复执行初始化操作、是否依赖某个前置服务完全就绪。我见过一个项目,后端容器和数据库容器同时启动,后端启动脚本里做了建表操作,结果每次重启都报“数据库还没准备好”,最后加了depends_on和健康检查才稳定。
6. 部署翻车之后我总结出的排查清单
最后一章,说说踩坑。设计到这里的你,大概率已经遇到了某种“奇怪的问题”——本地一切正常,服务器上五花八门。我在多次部署中总结了一套排查顺序,每一步都对应一类高频故障,拿来即用。
第一步,先分前后端。浏览器打开页面,F12看Network。如果HTML都能加载,只是接口报错,那问题在后端链路;如果页面都打不开,那问题在Nginx或静态资源。
第二步,后端链路用curl自测。在服务器本机执行curl http://127.0.0.1:8080/接口地址,如果通了,说明后端程序没问题,再检查Nginx代理配置。如果本机都不通,去看Java日志的堆栈信息,多半是数据库、Redis或者配置问题。
第三步,外部访问不通时,按“安全组→防火墙→端口监听”顺序排。安全组在云服务商控制台看,防火墙在服务器上systemctl status firewalld,端口监听用ss -lntp。
第四步,排查URL路径。确认前端发出的请求路径和Nginx匹配的location是否一致,确认proxy_pass带不带斜杠,这和后端接口实际路径是否匹配。路径问题占了部署问题的一半以上。
第五步,查配置项差异。把本地和生产环境的所有配置项列出来,逐一对比:数据库地址、Redis地址、文件上传路径、日志级别、域名配置。其中任何一个不一致,都可能导致不可预期的问题。
第六步,实在查不出就抓包。在服务器上tcpdump或者用curl -v看请求和响应详情。很多时候你以为“Nginx转发没问题”,实际上HTTP头和请求体早就不对了。
我自己养成的一个习惯是:每部署一次,就把这轮踩的坑记录到一个部署文档里。第二次部署相同类型的项目时,先翻一遍文档,10分钟之内就能把大部分坑规避掉。这个文档不光是给自己看的,团队新同事接手部署任务时,直接给文档,比自己踩一遍效率高得多。
前后端项目部署这件事,本质上就是在“本地环境”和“生产环境”之间搭一座桥。你不需要背下所有配置,只需要搞清楚每一层组件负责什么、请求是怎么一层层被转发的。把这条主线捋顺,换成任何技术栈、任何项目,你都能快速上手。最后分享一个小技巧:服务器上配置完Nginx,永远先跑一遍nginx -t再reload,这个习惯能帮你省掉一半的“改完配置Nginx挂了”的麻烦。