资讯动态

前后端分离项目部署实战:Spring Boot + Vue + Nginx + systemd方案解析

发布时间:2026/9/23 2:34:49 来源:尧图企业网站定制
前后端分离开发一时爽部署上线火葬场。这句话我念叨了好几年尤其是第一次自己从零搭部署环境的时候被各种小问题折腾到怀疑人生。前后端分离项目早已是Java后端领域的主流形态尤其Spring Boot Vue这套组合从若依这类快速开发框架到企业自研中台几乎成了标配。但开发环境跑得再顺畅一键部署到生产环境时Java后端该用什么样的守护方式前端打包后的静态资源怎么交给Nginx反向代理怎么配置才能不丢路径、不跨域对很多刚接触部署的朋友来说仍然是一堆绕不开的坑。这篇博文我想完整梳理一次实际可落地的部署方案后端用systemd来做守护进程前端交给Nginx托管静态文件并反向代理后端接口。我会把每一步的原理、配置、踩坑经验都写清楚不是那种只贴代码不解释的文章而是让看完的人能真正从头到尾把项目部署起来并且知道出了问题该怎么排查。如果你正在准备Java后端面试或者刚接手前后端分离项目的部署任务这篇内容应该能帮你省下不少摸索的时间。1. 部署方案整体设计与选型思路先把方案的大框架理清楚。前后端分离项目在部署层面最大的特点就是“两个独立进程”后端是一个Java应用Spring Boot打出的jar包前端是一堆纯静态文件Vue或React构建后的dist目录。两者不需要像传统单体JSP项目那样塞进同一个Tomcat而是各管各的进程与端口通过网络进行HTTP通信。1.1 为什么选systemd而不是脚本或Docker很多人一提到后端进程管理第一反应是nohup加个启动脚本或者干脆用Docker。这两种方式都能跑但在日常运维的体验上systemd的优势非常明显。先说说nohup的痛点。你用nohup java -jar app.jar app.log 21 把进程拉起来确实能跑但关掉终端后想找到这个进程、想看日志、想重启全得靠手动ps -ef | grep java去碰运气。更麻烦的是进程一旦因为OOM或被kill掉没有自动拉起机制等你发现接口挂了可能已经过去了几个小时。生产环境这么干迟早要背锅。再说说Docker。Docker本身很好但如果你们的运维基础设施还没有容器化平台甚至服务器资源比较紧张硬上一套Docker反而增加了学习成本和运维负担。而且Java应用在容器里跑JVM参数调优、内存限制、日志采集都需要额外适配。对于中小型项目或者传统企业服务器环境systemd作为Linux内建的服务管理工具轻量、零依赖、自带开机自启和崩溃重启是性价比最高的选择。systemd解决的核心问题包括开机自启动服务配置好后systemctl enable一条命令搞定不用写rc.local脚本。崩溃自动拉起设置Restarton-failure后进程异常退出时systemd会自动重启这对Java应用尤其重要因为JVM偶尔会因不可控因素比如堆外内存溢出、系统杀进程挂掉。统一管理日志通过journalctl查看日志或重定向到指定文件比直接输出到nohup文件更规范。依赖顺序控制可以声明Afternetwork.target确保网络就绪后再启动服务。1.2 Nginx在方案里的双重角色前端部分用Nginx很多人只知道它能托管静态文件其实在前后端分离架构里Nginx承担了两个角色。第一个角色是静态资源服务器。前端构建出来的index.html、js、css、图片等文件放到Nginx指定的root目录Nginx直接把这些文件响应给浏览器。这一层本身没有太多技术含量但有几个细节会影响性能比如Gzip压缩、静态资源缓存策略、history路由的try_files配置。第二个角色是反向代理服务器。后端Java应用监听在某个端口比如8080浏览器不能也不会直接访问这个端口而是让Nginx把/api前缀的请求转发到后端服务。这样前端代码里只需要写相对路径/api/user/list不需要知道后端真实IP和端口同时天然规避了跨域问题——因为浏览器看到的都是同一个域名是Nginx在后端做了代理转发。这套架构下的请求路径是浏览器 → Nginx80端口→ 静态文件直接返回 └→ /api请求代理转发 → Java后端8080端口1.3 服务器目录规划先做好很多部署前期看似顺手后期越改越乱往往是因为目录规划没花心思。我习惯的规划方式如下/app/ ├── backend/ │ ├── app.jar # 后端打包产物 │ ├── config/ # 外部化配置文件 │ ├── logs/ # 业务日志目录 │ └── bin/ # 启动辅助脚本 └── frontend/ └── dist/ # 前端构建产物后端jar包和配置文件分离是为了方便改配置不用重新打jar包前端dist目录独立是因为每次发版直接替换这个目录就行。我见过有人把前端文件解压到/usr/share/nginx/html把jar包丢在/root下的不是说不能跑而是项目一多维护起来自己都想报警。记住一个原则应用数据路径和系统路径彻底分开日志、配置、代码目录边界清晰。2. 后端打包与环境准备方案定下来后第一步是把后端Java应用变成可以直接在生产环境运行的状态。可别小看这一步很多部署问题的根源其实在打包和环境准备阶段就埋下了。2.1 Maven多环境打包的实际操作Spring Boot项目一般会区分开发、测试、生产环境配置通过application.yml里的spring.profiles.active来切换。以我常用的方式为例项目里会有这些配置文件src/main/resources/ ├── application.yml # 公共配置 ├── application-dev.yml # 开发环境 └── application-prod.yml # 生产环境打包时用Maven的profile特性来指定环境mvn clean package -DskipTests -Pprod对应的pom.xml里需要配置profileprofiles profile idprod/id properties activatedPropertiesprod/activatedProperties /properties /profile /profiles在application.yml里引用这个变量spring: profiles: active: activatedProperties这样打包出来的jar包内默认激活的就是生产环境配置。这里有一个需要注意的坑如果生产环境数据库IP、Redis地址等参数希望运维在部署时临时调整那就不要把这些敏感配置写死在jar包内的application-prod.yml里而是利用Spring Boot的外部化配置机制。把application-prod.yml放到jar包同级目录的config文件夹下jar包启动时会优先读取外部配置覆盖jar包内的同名配置。这样相当于“打包和配置分离”换环境和改配置都不需要重新build。2.2 JDK与基础环境检查生产服务器上装JDK强烈建议用OpenJDK版本尽量和开发环境保持一致否则容易出现编译版本比运行版本高导致的UnsupportedClassVersionError。常见的坑有开发用JDK 17生产只装了JDK 8启动必失败。用高版本JDK编译默认字节码版本过高生产低版本JVM跑不了。检查命令java -version输出里看openjdk version和LTS标识。Spring Boot 2.x对应JDK 8或11Spring Boot 3.x要求JDK 17及以上这个对应关系要记清楚。部署前先在服务器上跑一个最简单的Spring Boot测试项目确认端口能通、JVM正常再开始部署正式项目能帮你排除掉环境层面的干扰因素。除了JDK还要检查使用到的中间件是否就绪MySQL确认服务启动且应用配置的账号密码有权限访问目标库。Redis确认端口可达如果设置了密码配置里要对应修改。依赖的其他服务如消息队列、文件存储服务网络连通性提前验证。用telnet 127.0.0.1 3306或nc -vz 127.0.0.1 6379做快速验证比等到应用启动报错再去排查高效得多。2.3 后端启动核心参数与JVM调优Java进程在生产环境的启动参数是门学问。简单java -jar app.jar能跑但性能和稳定性都谈不上。我整理了一份相对稳妥的启动模板java -Xms512m -Xmx512m \ -XX:UseG1GC \ -XX:MaxGCPauseMillis200 \ -Dfile.encodingUTF-8 \ -Dspring.profiles.activeprod \ -jar app.jar \ --spring.config.additional-locationclasspath:/,file:/app/backend/config/参数含义说明-Xms和-Xmx初始化堆内存和最大堆内存建议设成一致避免JVM运行中动态伸缩堆大小带来性能波动。服务器内存2G的话给到512m或1G比较合理留足余量给操作系统和其他进程。-XX:UseG1GCJDK 11默认就是G1但显式声明能防止将来换JDK版本后行为不一致。-Dfile.encodingUTF-8中文环境下不设置这个可能出现日志乱码、导出文件名乱码等问题。-Dspring.profiles.activeprod显式指定激活环境即使jar包内默认值错了也能纠正。--spring.config.additional-location重点讲一下这个参数可以额外加载配置目录而且优先级高于jar包内的配置。比如config目录下的application-prod.yml里有数据库密码它会覆盖jar包内的同名配置。3. systemd管理Java后端服务实操后端环境准备好之后正式进入重头戏用systemd把Java应用注册成系统服务。这一节是整个部署方案里最核心的部分配置正确后续运维会非常省心。3.1 Service Unit文件详细参数解析在/etc/systemd/system/目录下创建一个service文件名字通常和项目对应比如myapp.service。文件内容如下[Unit] DescriptionMy Application Server Afternetwork.target mysqld.service redis.service Wantsnetwork.target [Service] Typesimple Userappuser Groupappuser WorkingDirectory/app/backend EnvironmentJAVA_HOME/usr/local/jdk-17 EnvironmentSPRING_PROFILES_ACTIVEprod ExecStart/usr/local/jdk-17/bin/java -Xms512m -Xmx512m -XX:UseG1GC -jar /app/backend/app.jar ExecStop/bin/kill -s TERM $MAINPID SuccessExitStatus143 Restarton-failure RestartSec10 StartLimitIntervalSec60 StartLimitBurst3 UMask0022 [Install] WantedBymulti-user.target逐个拆解这些参数的含义下面这些是整个配置的关键User和Group指定用哪个系统用户运行Java进程。强烈不建议用root直接跑应用安全风险太大。可以在服务器上单独建一个appuser用户并把/app目录交给这个用户。Typesimple默认值表示ExecStart启动的进程就是主进程systemd不会做额外检测。Spring Boot的jar包启动后前台一直挂着正好匹配这个类型。WorkingDirectory设置工作目录jar包内如果有相对路径读取文件的操作会基于这个目录定位。Environment注入环境变量。如果服务器上配置了全局JAVA_HOME可以不加但如果JDK装在非标准路径这里指定最稳妥。SPRING_PROFILES_ACTIVE环境变量和启动参数-Dspring.profiles.activeprod二选一即可。ExecStart实际的启动命令。这里注意如果用到%或$这类字符需要适当转义命令行里写了变量尽量用环境变量代替。ExecStop停止命令。默认systemd会向主进程发送SIGTERMSpring Boot会捕获并优雅停机关闭线程池、释放数据库连接池。写这么一串是为了确保优雅停止同时兼容一些特殊情况。SuccessExitStatus143Spring Boot优雅停机时进程退出码可能是14312815对应SIGTERM如果不标记为成功systemd会认为服务启动后崩溃退出从而触发不必要的重启逻辑。Restarton-failure进程异常退出才重启。业务代码抛出未捕获异常导致进程退出或者被系统杀掉都会自动拉起。但如果主动执行systemctl stop则不会触发重启。RestartSec10重启前等待10秒防止代码有循环崩溃问题时无限快速重启把服务器CPU打满。StartLimitIntervalSec和StartLimitBurst60秒内最多重启3次超过后systemd会放弃并标记服务为失败避免“无限重启”导致更严重的问题。3.2 服务启动、停止与开机自启配置文件写好后需要用命令真正让systemd生效# 重新加载systemd配置让新service文件生效 sudo systemctl daemon-reload # 启动服务 sudo systemctl start myapp # 查看服务状态 sudo systemctl status myapp # 设为开机自启 sudo systemctl enable myapp查看服务状态时重点关注几个字段Active: active (running)服务是否正常运行Main PID:主进程PIDCGroup进程所属的控制组Memory:系统显示的内存占用如果看到状态是Active: failed立刻用journalctl -u myapp -n 50 --no-pager查看末尾50行日志通常错误信息就在里面。最典型的一类错误是路径不对导致Error: Unable to access jarfile或者端口被占用导致Port already in use。3.3 systemd与日志管理的配合日志是生产环境排查问题的命脉。systemd默认把服务的标准输出和标准错误都收集到journal里用journalctl命令就能直接看日志# 查看服务全部日志 journalctl -u myapp # 查看最近1小时的日志 journalctl -u myapp --since 1 hour ago # 跟随日志输出类似tail -f journalctl -u myapp -f # 重启后的日志排查 journalctl -u myapp --since today但生产环境我建议双管齐下journal保留一部分便于快速查看同时让业务日志落到独立文件里。Spring Boot的logback-spring.xml里把日志按天滚动输出到/app/backend/logs/目录这样方便用采集工具如Loki、ELK做集中式日志收集也方便直接去文件里定位问题。有个细节需要留意systemd启动的服务默认UMask是0022这意味着目录和文件的默认权限是755/644。如果你的日志文件交给其他用户读取这个权限没问题如果不想暴露给所有用户可以在service里设置UMask0027让日志默认600权限。3.4 服务版本更新时的优雅交替每次发布新版本不能简单粗暴地kill掉旧进程再启动新的。常规操作流程是# 停止服务 sudo systemctl stop myapp # 备份旧jar包可选便于快速回滚 mv /app/backend/app.jar /app/backend/app.jar.bak # 上传新jar包 # 用scp或rsync上传到/app/backend/app.jar # 重新加载配置如果没改过service文件可以跳过 sudo systemctl daemon-reload # 启动服务 sudo systemctl start myapp # 确认启动成功且接口正常 curl -v http://127.0.0.1:8080/actuator/health这套流程虽然多敲几条命令但每一步都可控。生产环境切忌用kill -9强杀Java进程Spring Boot的优雅停机机制能把异步任务、消息队列消费、数据库连接池都完整释放硬杀可能丢数据或损坏本地状态。实际运维中我都习惯写一个/app/backend/bin/restart.sh脚本#!/bin/bash sudo systemctl stop myapp sleep 2 sudo systemctl start myapp sleep 5 status$(systemctl is-active myapp) if [ $status active ]; then echo 服务启动成功 else echo 服务启动失败查看日志 journalctl -u myapp -n 50 --no-pager exit 1 fi省得每次重复敲命令。4. Nginx托管前端与反向代理配置后端服务跑起来之后开始配置Nginx。这一步的目标是访问服务器IP或域名就能看到前端页面并且页面上发的/api请求能正确转发到Java后端。4.1 静态资源部署与history路由适配Vue或React构建后的产物放到/app/frontend/dist目录后Nginx的server配置核心就三块静态资源路径、SPA路由刷新支持、接口反向代理。先看一个完整的server配置示例server { listen 80; server_name example.com; # 开启gzip压缩能显著减小前端资源传输体积 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; gzip_min_length 1k; root /app/frontend/dist; index index.html; # 前端history路由支持找不到对应文件就回退到index.html location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 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_set_header X-Forwarded-Proto $scheme; } # 静态资源缓存策略 location /assets/ { expires 30d; add_header Cache-Control public, immutable; } }这里面的关键细节我逐个解释。try_files $uri $uri/ /index.html;这一行是前后端分离部署最容易踩坑的地方。如果不加这行Vue Router使用history模式时访问/user/list这个前端路由Nginx会去/app/frontend/dist/user/list找文件找不到就返回404。加上回退到index.html后Nginx会把所有非静态文件的请求交给前端路由去处理刷新页面就不迷路了。如果你用的是hash模式URL带#理论上不需要这行但生产环境推荐用history模式并配好这行。location /api/里proxy_pass http://127.0.0.1:8080/末尾那个斜杠是无数人踩坑的重灾区。带斜杠和不带斜杠转发效果天差地别。带斜杠访问/api/user/list时Nginx会去掉/api把请求转发为/user/list后端的Controller就不用加/api前缀。不带斜杠访问/api/user/list时Nginx直接把完整路径发过去后端接口必须是/api/user/list才能匹配上。所以这里必须和后端代码约定一致。如果后端所有接口都带/api前缀就用不带斜杠的写法如果后端接口本身没有统一前缀就在Nginx里用带斜杠方式剥离。我在实际项目里更推荐后者因为很多快速开发框架比如若依本身就约定好/api前缀前后端代码都好维护。4.2 反向代理配置里的header细节反向代理时加的这几个header不能省Host $host把浏览器的域名原样传给后端如果后端代码依赖这个header做URL拼接或安全校验这个必须加上。X-Real-IP $remote_addr记录真实客户端IP。如果代理不加后端拿到的永远是127.0.0.1做IP限流或审计时就会失灵。X-Forwarded-For标准代理链IP头后端配合server.forward-headers-strategyframework能从请求里取出真实IP。X-Forwarded-Proto标记原始请求协议是http还是https。如果后端有重定向逻辑比如OAuth授权回调没有这个头可能会把https重定向成http导致回调失败。4.3 Nginx缓存与性能参数调优前端静态资源的性能优化很大一部分靠Nginx的缓存配置。上面配置里的location /assets/就是针对Vue打包后带哈希指纹的静态文件做的强缓存因为这些文件名每次构建都会变化一旦上线后永远不会变可以放心缓存30天。而index.html本身不能设长缓存因为它是前端发版的入口如果浏览器端被强缓存后端和Nginx配置更新了用户还是要手动刷新才能看到新版本。所以index.html保持默认不缓存依赖Cache-Control: no-cache让浏览器每次向服务器确认版本。Nginx还有几个全局性能参数值得调一下worker_processes auto; worker_connections 1024; keepalive_timeout 65; client_max_body_size 50m;worker_processes autoNginx工作进程数设为auto可按CPU核心数自动分配。client_max_body_size默认1m如果项目有文件上传功能上传大文件会直接报413这个要根据业务调大。5. 常见问题与排查技巧实录部署过程中最磨人的就是各种莫名其妙的问题。我把自己这几年踩过、帮同事排查过的高频问题整理成了速查表每个都附上排查思路。5.1 systemd服务启动失败排查典型的启动失败问题有这几种。报错Error: Unable to access jarfile说明ExecStart里的jar包路径写错了或者jar包还没上传到服务器。先确认文件是否存在ls -l /app/backend/app.jar报错Port 8080 already in use说明端口被别的进程占了。用命令找到占用者ss -tnlp | grep 8080如果是历史遗留进程kill掉再启动如果是另一个服务在跑就要考虑改端口。报错Failed to determine a suitable driver class一般是配置文件里的数据库连接信息没加载到检查SPRING_PROFILES_ACTIVE环境变量是否设置正确以及config目录下的配置文件路径是否在启动参数里声明。服务状态显示 activating (auto-restart)这是restart循环启动了。用journalctl -u myapp -n 100看看日志里报什么错按照StartLimitBurst限制如果持续失败systemd会在超过次数后放弃这时反而更容易看到真实的启动报错信息。5.2 前端页面打开404或刷新后404这个问题的90%原因都是try_files没配对。打开首页正常的点路由跳转也正常但一按F5刷新就404这就是典型的SPA history路由问题。检查Nginx里location /的配置location / { try_files $uri $uri/ /index.html; }如果依然不行确认前端构建模式Vue Router创建时是否用了createWebHistory()如果用hash模式则URL带#号不存在路由刷新404的问题。还有一种情况是dist目录路径不对导致Nginx找到第一个文件就读取失败。可以临时开Nginx的error日志tail -f /var/log/nginx/error.log看404时实际请求的文件路径是哪个就能快速定位。5.3 接口502 Bad Gateway的排查方向502意味着Nginx转发请求时得不到后端Java服务的有效响应。按顺序排查后端服务是否还活着systemctl status myapp看状态或者curl -v http://127.0.0.1:8080/api/xxx直接访问后端。如果后端活着但curl就超时可能是后端线程池满了或接口响应太慢看后端日志。如果后端也通看Nginx的proxy_pass路径是否正确转发后的路径和后端实际接口是否匹配。502和504要分清502一般是连接建立不起来后端挂了或端口不通504是连接建立了但后端处理超时网关等待超过proxy_read_timeout。5.4 接口跨域问题虽然Nginx反向代理已经统一了域名减少了跨域场景但如果后端开了CORS配置有时反而会出现预检请求报错。我遇到的情况是后端配置了allowOrigins(*)但Nginx代理时没有把Origin头传递过去导致浏览器预检请求失败。排查思路看浏览器Network面板的请求确认请求头里有没有Origin响应头里有没有Access-Control-Allow-Origin。如果前后端同域名就不应该涉及跨域反而要把后端无脑全开CORS的方式规范起来只在开发环境打开生产环境交给Nginx处理。5.5 Java进程内存溢出排查这是个稍微进阶的问题但值得提前预防。Java进程跑一段时间后突然OOMsystemdRestarton-failure会自动拉起来但如果频繁重启就要查根本原因了。建议在JVM参数里加上OOM时自动dump内存快照-XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/app/backend/logs/heapdump.hprofOOM发生时自动生成dump文件用VisualVM或MAT分析大对象堆基本就能定位是内存泄漏还是分配不足。如果只是堆内存不够调大-Xmx如果是泄漏就得去查代码里的集合缓存、静态变量、或数据库连接池配置了。6. 部署完成后的一些额外建议部署服务稳定跑起来只是第一步还有一些工作会在后续运维里极大影响体验这里顺带提几句。6.1 定期检测服务健康状态生产环境比的是防御能力。写一个简单的健康检查脚本每分钟定一次活#!/bin/bash urlhttp://127.0.0.1:8080/actuator/health code$(curl -s -o /dev/null -w %{http_code} $url) if [ $code ! 200 ]; then echo $(date) 健康检查失败HTTP状态码: $code /app/backend/logs/healthcheck.log fi配合crontab定时执行加上systemd服务本身的restart策略能挡住大部分意外故障。6.2 利用systemd的环境变量覆盖配置后续改数据库IP之类的事情不用去动jar包和service文件可以在service文件里声明环境变量EnvironmentDB_HOST10.0.0.5 EnvironmentDB_PORT3306然后在Spring Boot的application.yml里用${DB_HOST:127.0.0.1}占位符读取这样环境变了改service文件重新加载即可非常灵活。6.3 把Nginx配置纳入版本管理很多团队的前端有Git仓库后端有Git仓库但服务器的Nginx配置散落在各个机器上出问题只能去机器上查。建议把Nginx的conf目录整个纳入git管理配合软链接指向/etc/nginx/conf.d/下的实际配置文件发布时直接把配置文件作为版本化产物一并管理回滚也会方便很多。7. 写在最后的实操体会最后换回老博主的口吻说几句实在话。前后端分离部署这件事本身的门槛并不在“技术难度”而在于“细节密度”。systemd配置也就是那十几行Nginx配置也就十几行把它们组合在一起却需要理顺很多前提jar包怎么打、配置怎么外置、目录怎么规划、日志怎么管理、路径怎么匹配、跨域怎么避免。任何一个环节出了问题表现出的现象都可能是抽象的“页面打不开”或者“接口报错”但真正的病根可能藏在systemd的日志里也可能在Nginx的error log里。所以排查问题不要靠猜一层层按链路去验证浏览器请求到NginxNginx转发到JavaJava依赖的数据库和中间件每一步都能用curl简单探测问题定位到具体环节解决起来就快了。我自己刚开始做主前后端分离部署时也是把能踩的坑几乎踩了个遍jar包用JDK17编译拿到JDK8服务器上跑直接ClassVersionErrorNginx的proxy_pass路径末尾少了斜杠接口访问直接404忘记配try_files页面一刷新就白屏systemd没设置Restart半夜后端进程挂了直到第二天早上才被用户发现。这些看似小问题每一件都足以让运维足够“酸爽”。希望这篇内容能帮你少走一些弯路在部署这套技术栈时心里有一个完整的框架知道该做什么、为什么这么做、出了问题去哪里查。如果你按这个流程走一遍下来应该会发现部署前后端分离项目其实没有传说中那么玄乎。

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

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

免费获取报价