Java全栈项目部署上线实战:从Spring Boot到Nginx全流程
做了这么多年全栈开发带过的项目从几十个人的内部系统到日活过万的业务平台都有我越来越觉得“部署上线”这四个字才是整个开发流程里最考验基本功的一环。代码在本地跑得再欢打不成包、上不了线、扛不住访问前面所有工作都等于白做。这一课《项目部署上线》是整个Java全栈教程的第50课也是把前面所有知识串成一条完整链路的最后一课从Spring Boot后端、Vue前端再到MySQL、Redis、Nginx整套体系怎么在一台干净服务器上从零跑起来我会把每一步的取舍逻辑和我在真实项目中踩过的坑都说清楚。不管你是在准备Java面试、走Java全栈学习路线还是手头刚做完一个想给朋友或面试官演示的项目这篇内容都值得你从头到尾走一遍。它能帮你解决一个特别实际的问题项目写完了怎么让它真正“活”在一台大家都能访问的服务器上而不是永远停留在localhost。1. 部署这件事其实拼的是准备功夫很多人第一次部署项目习惯是拿到服务器就开干先装环境再传代码然后各种报错改一步试一步最后折腾一晚上还没跑起来。我早年也这么干过后来带团队之后学乖了——部署前花半小时做规划和检查比部署后花三小时排查问题要划算得多。1.1 上线前先把方案选型定下来部署方案没有绝对的标准答案但你得根据项目规模和预算选一条最合适的路。第一类是最典型的单体全栈项目一个Spring Boot后端、一个Vue前端、一个MySQL、一个Redis。这种项目用一台2核4G的云服务器就能跑得很稳成本低维护也简单。这也是我这节课主要讲的场景。第二类是稍微复杂一点的微服务或多模块项目比如Eureka注册中心加多个业务服务。这种最好用Docker Compose统一编排不然每个服务手动启一遍光是环境一致性就够你头疼。Docker能把Java环境、依赖、启动命令全部打包到一个镜像里换服务器部署基本无痛。第三类是你有多个模块、需要频繁发布、团队协作了那就得上Jenkins或GitLab CI这套自动化流水线再加一个Nexus私服存构建产物。这个方向适合工作后接触个人项目现阶段不需要折腾那么重。无论你选哪种方案有几点是通用的数据库要和应用代码分开管理配置文件里不要写死任何环境相关的信息服务器上只保留运行所需的最小工具集合。千万别图省事直接在服务器上装个IDEA然后用IDE跑生产代码这种操作我见过出问题的时候简直是一场灾难。1.2 服务器和环境的统一规划假设你已经买好了一台云服务器接下来最容易被忽略的是环境变量的统一规划。先检查服务器发行版。我用得比较多的是CentOS 7.9和Ubuntu 20.04两个系统在命令上略有差异但核心逻辑一样。新的服务器到手先做三件事更新系统源、创建专用的部署用户、配置SSH密钥登录。# Ubuntu/Debian sudo apt update sudo apt upgrade -y # CentOS/RHEL sudo yum update -y创建部署用户的目的是隔离权限不要所有的服务都拿root跑。我习惯创建一个叫deploy的用户把项目的所有文件都放在/home/deploy/apps/下面结构大概是这样/home/deploy/apps/ ├── backend/ # 后端jar包和配置文件 ├── frontend/ # 前端静态资源 ├── logs/ # 统一日志目录 └── backup/ # 数据库备份和旧版本包这个目录结构看起来简单但能让后续的日志查看、备份、回滚都变得特别顺手。你别小看这一步我见过很多项目文件乱扔在/root或者/tmp下面半年之后再想找到某个版本的包愣是能花上一个下午。环境规划里还要提前定好端口和防火墙策略。我的习惯是后端应用端口用8090Nginx用80和443MySQL用3306Redis用6379。云服务器控制台的安全组只对外开放80、443其他所有端口一律禁止外部访问后端接口全部走Nginx反向代理。这样做的理由很实际攻击者没法直接扫描到你的应用端口数据库和Redis更是完全藏在内网里。2. 后端打包与Java应用落地后端服务是整个系统的心脏这一部分我重点讲Spring Boot项目的打包、启动和守护。很多新手最容易在这里翻车因为本地IDE帮你把一切都处理好了你根本不知道Java进程在真实环境里有多“野”——它不会自己重启不会自己清日志也不会帮你检查端口占用。2.1 Maven多环境打包别再手动改配置本地开发和线上环境的数据库地址、Redis地址、日志级别肯定不一样你要是每次发布前手动改一遍application.yml早晚会出事。正确做法是用Maven的profile功能做多环境配置。第一步在pom.xml里定义profilesprofiles profile iddev/id properties activatedPropertiesdev/activatedProperties /properties /profile profile idprod/id properties activatedPropertiesprod/activatedProperties /properties /profile /profiles第二步在application.yml里指定使用哪个profilespring: profiles: active: activatedProperties这个activatedProperties占位符在Maven打包时会自动替换成你指定的profile名称。然后准备两个配置文件application-dev.yml和application-prod.yml各自维护自己环境的配置。打包的时候只需要# 跳过单元测试打生产包 mvn clean package -Pprod -DskipTests用这种方式你会彻底告别“上线前记得把localhost改成云服务器IP”这种高危操作。我个人的额外习惯是在prod配置里把数据库密码通过环境变量注入而不是直接写在yml文件里。因为yml文件是有可能被上传到Git仓库的密码一旦进了版本历史就很难彻底清除了。打包成功之后target目录下会生成一个xxx.jar这个jar就是产物。注意看下jar包大小一般Spring Boot项目大概在50MB到100MB之间。如果特别小很可能你用了spring-boot-maven-plugin配置错误导致依赖没有打进去。2.2 让Java应用跑起来从nohup到systemd最简单的启动方式就是Java直接跑java -jar backend.jar但这种方式有个致命问题关闭终端应用就死了。而且一旦进程因为内存溢出或其他原因退出没人帮你把它拉起来。我推荐用systemd来管理Java服务它能把Spring Boot变成一个标准的系统服务支持开机自启、崩溃自动重启、日志统一管理比nohup甩几条街。先创建service文件在/etc/systemd/system/backend.service里放下面这段配置[Unit] DescriptionJava Backend Service Afternetwork.target mysql.service redis.service [Service] Typesimple Userdeploy WorkingDirectory/home/deploy/apps/backend ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /home/deploy/apps/backend/backend.jar Restartalways RestartSec10 StandardOutputappend:/home/deploy/apps/logs/backend.log StandardErrorappend:/home/deploy/apps/logs/backend-error.log [Install] WantedBymulti-user.target配置里有几个关键点得说清楚Afternetwork.target mysql.service redis.service表示这两个服务启动之后才尝试启动backend避免应用启动了但数据库还没就绪导致连接失败。Restartalways是核心进程只要异常退出systemd会等10秒自动拉起。StandardOutput和StandardError配置直接把日志输出到指定文件比你自己写logback文件配置简单多了。配置好之后执行sudo systemctl daemon-reload sudo systemctl enable backend.service sudo systemctl start backend.service sudo systemctl status backend.servicestatus显示active (running)就说明服务起来了。想看实时日志就执行journalctl -u backend -f或者直接查看logs目录下的backend.log。部署新版本时候的操作顺序我也说一下这个流程我踩过太多次坑才总结出来先把新jar传到服务器上备份旧jar再重启服务。直接覆盖旧jar然后重启万一新版本启动失败你想回滚只能干瞪眼。# 先备份旧版本 cp backend.jar backend-$(date %Y%m%d).jar # 覆盖新版本后重启 sudo systemctl restart backend.service2.3 启动参数与内存分配的经验值Java应用最怕的就是内存没规划好。2G内存的服务器你给JVM配了1.5G的堆内存那其他进程基本就废了。我这里给个经验值参考服务器内存JVM堆内存-Xmx初始堆内存-Xms适合场景2G512M - 1G256M - 512M单体项目低并发4G1G - 2G512M单体项目中等并发8G2G - 4G1G较大项目或微服务-Xms和-Xmx设置成一样的好处是避免JVM运行时动态扩容带来的性能波动但这个值主要看服务器资源够不够。如果内存富裕就直接设成相同值。启动参数里还有一个容易被忽略的东西就是时区。很多服务器默认时区是UTC而你的数据库和前端用的都是北京时间就会出现一个诡异的现象后台看数据创建时间差了8小时。JVM层面可以这样设置ExecStart/usr/bin/java -Duser.timezoneAsia/Shanghai -Xms512m -Xmx1024m -jar /home/deploy/apps/backend/backend.jar-JVM参数-Duser.timezoneAsia/Shanghai能解决一部分时区问题但根本解法还是MySQL连接串里加上serverTimezoneAsia/Shanghai同时操作系统时区也用timedatectl set-timezone Asia/Shanghai校正一遍。三者一致才能彻底避免时间偏移。3. 前端构建与Nginx反向代理前端部署看起来简单不就是一个静态文件服务器嘛。但实际上要处理的问题不少API请求往哪走、历史路由模式要不要特殊处理、静态资源缓存策略、HTTPS证书随便一个都要花时间调。3.1 Vue项目的生产构建前端构建几乎是零成本的前提是你本地环境是好的。在项目根目录执行npm install npm run build构建完成后会生成一个dist目录。如果用的是Vitedist里是index.html和assets文件夹如果是Vue CLI结构类似。你会发现assets里的JS和CSS文件名带了哈希值比如index.3a2f9b7c.js这是Webpack或Vite内容哈希的特性。只要文件内容变了文件名就变这个特性配合Nginx的缓存策略可以做到资源永久缓存、文件更新即时生效。构建过程中最容易出问题的是接口地址。你在开发环境用axios请求的是/api这个/api通过开发服务器的proxy代理到后端。生产环境没有vite的proxy机制了你要么把头写死成全地址要么用Nginx做代理。我的建议永远是前端把请求写成相对路径/api具体代理到哪台后端机器交给Nginx处理。这样前端构建产物在任何环境都能复用后端地址变了只需要改Nginx配置。构建完成后把dist文件夹传到服务器的前端目录然后配置Nginx。3.2 Nginx配置静态资源与API转发的正确姿势Nginx在前端部署里的角色有两个托管静态页面和反向代理API请求。它把两者结合在一个server块里这是最常见的部署方案。下面是我实际项目中一直在用的配置模板server { listen 80; server_name yourdomain.com; # 前端静态资源目录 root /home/deploy/apps/frontend/dist; index index.html; # 前端路由的history模式关键配置 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存带哈希文件缓存一年 location /assets/ { expires 1y; add_header Cache-Control public, immutable; } # API反向代理 location /api/ { proxy_pass http://127.0.0.1:8090; 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; } }try_files $uri $uri/ /index.html这行是核心。Vue Router如果用的是history模式访问/vue-router的history模式必须配合这个配置否则刷新页面就会404。/assets/的缓存策略是借助文件名哈希实现的我前面提到过文件内容变了哈希就变浏览器自然请求新文件所以可以放心设置一年缓存。/api/的代理转发也有讲究。proxy_pass后面可以直接跟http://127.0.0.1:8090也可以跟http://127.0.0.1:8090/区别在于路径的处理带结尾斜杠转发时会去掉location匹配的/api前缀不带结尾斜杠则保留完整uri。这个细节我吃过亏建议你实际项目里根据后端接口路径设计来选择。如果后端Controller的手机号写的是RequestMapping(/api/user)那proxy_pass就不要带结尾斜杠或者后端统一处理前缀两种方式别混用。使用后端本机地址127.0.0.1的前提是Nginx和后端服务在同一台服务器。如果前后端分离部署到不同机器这里就要改成后端服务器的内网IP同时保证两台机器的网络安全组放通对应端口。3.3 HTTPS证书配置几分钟搞定现在的项目不上HTTPS浏览器直接预警“不安全”很多功能还会被限制。给Nginx配HTTPS我用的是Lets Encrypt的免费证书配合certbot自动续期全程完全不花钱。如果服务器上已经有域名且解析到了这台服务器可以执行sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d yourdomain.comcertbot会自动帮你修改Nginx配置把80端口的请求重定向到443并自动配上证书。最重要的是它还会设置定时续期任务。Lets Encrypt证书有效期是90天到期前需要自动续期否则HTTPS就失效了。证书配好之后一个标准的HTTPS server块大概长这样server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; # 其余配置和HTTP一致 }有件事必须提醒升级HTTPS之后前端代码里所有请求必须是https或者相对协议//api绝不能写死http。否则浏览器会拦截混合内容出现“页面能打开但接口全挂”的诡异问题。排查这种问题最直接的方法是打开浏览器开发者工具看Console里的混合内容警告。4. 数据库、缓存与数据迁移我见过不少人部署的时候只关注应用本身数据库随便装一个密码设个123456表结构手动跑一遍SQL就完事了。这种部署方式一旦出问题后果非常严重。数据库这一层值得多花点时间做扎实。4.1 初始化数据库与账号体系MySQL装好之后第一件事就是不要直接用root连业务库。我每次初始化数据库都是按照这套顺序来的-- 创建业务用户只给业务库的权限 CREATE USER app_userlocalhost IDENTIFIED BY 你的强密码; CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL PRIVILEGES ON app_db.* TO app_userlocalhost; FLUSH PRIVILEGES;utf8mb4对照上mysql8的默认字符集是utf8mb4但你手动建库的时候还是要显式声明一下避免某些老版本MySQL默认latin1。字符集错了中文全变乱码后面再改就麻烦了。权限这里我特意只授权了localhost也就是说只有本机应用才能连接MySQL。如果你的后端服务和数据库不在同一台机器这里就要授权给后端服务器的内网IP同时无论如何都不要授权成%所有来源。表结构初始化我推荐把整个数据库的表结构、初始数据全部导出成SQL脚本管理起来。可以用Navicat或mysqldump导出然后在新环境里执行。更规范的做法是直接用Flyway这类数据库版本管理工具它能在应用启动时自动检查数据库版本把项目代码和数据库结构变更统一纳入版本管理。个人项目如果嫌麻烦至少要把建表SQL脚本放到项目里留档。4.2 Redis配置要点Redis在Java全栈项目里一般承担缓存和会话管理的职责。部署的时候第一件事就是设置密码。默认Redis没密码只要端口对外开放任何人都能连上来执行命令这等于直接裸奔在公网上。redis.conf里的核心配置我就写三个通用的# 只监听本机 bind 127.0.0.1 # 端口 port 6379 # 密码 requirepass your-redis-passwordbind 127.0.0.1非常关键只监听本机那么外部IP想连也连不上和防火墙设置形成了双重防护。如果你的后端在另一台机器上需要远程访问Redis那也建议同时开启防火墙限制来源IP不要图省事把所有地址都放进来。Spring Boot连接Redis的配置里别忘加上密码spring: redis: host: 127.0.0.1 port: 6379 password: your-redis-password timeout: 3000msRedis是否配持久化取决于你的业务。如果是纯缓存业务比如热点数据失效就重新查库那配置RDB快照就可以了恢复速度快、性价比高。如果Redis里存了关键业务数据比如某些项目的分布式锁状态或者购物车内容那就得开AOF虽然数据恢复慢一点但更可靠。个人项目阶段RDB足够。4.3 数据迁移与备份策略数据迁移是个大坑尤其是从本地环境把数据库搬到云服务器。最稳妥的方法是先用mysqldump导出整个库再在新服务器的MySQL里导入。# 本地导出 mysqldump -u root -p --default-character-setutf8mb4 --single-transaction --set-gtid-purgedOFF app_db app_db.sql # 服务器导入先创建好数据库再执行 mysql -u app_user -p app_db app_db.sql--default-character-setutf8mb4这个参数会直接决定导出文件编码不加它遇到特殊字符很容易导出乱码。--single-transaction是为了在导出时不锁表生产库也要用这个参数。备份策略这块我自己的节奏是每天凌晨全量备份数据库保留最近7天文件。服务器上写一个简单脚本配合crontab定时任务就能跑#!/bin/bash BACKUP_DIR/home/deploy/apps/backup DB_USERapp_user DB_PASS你的数据库密码 DB_NAMEapp_db DATE$(date %Y%m%d_%H%M%S) mysqldump -u$DB_USER -p$DB_PASS --single-transaction --set-gtid-purgedOFF $DB_NAME | gzip $BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz find $BACKUP_DIR -name *.sql.gz -mtime 7 -delete这一段代码实测下来非常稳数据量不大的情况下整个备份过程也就几秒钟对业务几乎没有影响。做完之后你要做的就是把脚本加到crontab里crontab -e # 每天凌晨2点执行备份 0 2 * * * /home/deploy/apps/backend/backup.sh有条件的建议再加一步异地备份。比如每天把备份文件用scp或ossutil同步到对象存储。数据这东西多一份备份就多一份保障服务器被误删除或者磁盘损坏的情况虽然少见但真遇到一次就能让你崩溃。5. 上线后才是真考验常见问题排查实录项目部署完浏览器里能打开首页接口能返回数据你以为就结束了吗恰恰相反真正的考验从这一刻才开始。我把自己这几年线上问题排查的经验整理一下很多都是花了大半夜才总结出来的教训。5.1 502 Bad Gateway的排查思路502应该是上线后最常见的报错页面了。它的本质是Nginx作为反向代理没法从后端获取有效响应。排查顺序我认为应该这样来先看后端服务到底还活着没有sudo systemctl status backend.service如果状态不是active(running)那就是服务崩了直接查看日志文件看崩溃原因。常见原因是端口占用、数据库连不上、内存不足。如果服务活着接着看后端端口是否真的在监听netstat -tlnp | grep 8090如果端口没监听可能是应用启动失败但systemd还没来得及更新状态这时候journalctl -u backend -f能看实时日志。如果端口正常监听那大概率是Nginx配置问题重点检查proxy_pass的地址和端口是否被注释或者写错改完别忘了nginx -t先测试配置再reload。另外一个隐蔽的原因是Nginx和后端之间的超时。如果服务端某个接口执行超过60秒Nginx默认的proxy_read_timeout到了就会主动断开表现也是502。这种情况要么优化接口耗时要么适当调整location /api/ { proxy_read_timeout 60s; proxy_connect_timeout 10s; }502排查那句话我说了很多遍先看服务进程再看端口最后看日志。按这个顺序来大多数问题五分钟能定位。5.2 内存与CPU飙升问题上线第二天早上被报警信息吵醒打开监控面板一看内存爆了——这种情况我经历过太多次了。Java服务内存占用过高第一步用jps找到进程PID然后# 查看堆内存使用情况 jmap -heap pid # 导出堆转储文件等待GC的现场 jmap -dump:formatb,fileheap.hprof pid # 查看线程状态 jstack pid thread_dump.txt这些工具在JDK自带目录的bin下面线上排查时用起来特别顺手。大家最常见的OOM原因有这么几类一次性查了太多数据没分页、缓存没有设置过期时间、连接池配置过小导致线程全部阻塞堆积。修复起来不算复杂但要学会用jmap和jstack定位是哪个类哪个方法导致的这个能力在Java面试里也经常被问到。CPU飙升的排查更直接# 按CPU排序看进程 top -c # 查看进程内哪个线程占用高 top -Hp pid拿到线程PID后把它转成十六进制再用jstack查看线程栈printf %x\n 线程PID jstack pid | grep -A 30 nid0x十六进制基本就能定位到具体代码。实践里最常见的场景是某个循环里频繁调用外部接口或SQL没走索引。5.3 时区、字符集、日志几个小坑这些小问题每个都不难解决但凑在一起足以让人怀疑人生。时区问题前面已经说过了JVM、MySQL连接串、操作系统三层必须一致有一个不匹配就会出现“数据库时间对不上”“前端显示时间少了8小时”这种脑壳疼的问题。字符集问题最常见的表现是接口返回的中文乱码。检查三处Linux系统的locale执行locale命令看LANG变量、MySQL数据库的character_set_server、Spring Boot的server.servlet.encoding配置。一般来说系统locale设为UTF-8数据库字符集用utf8mb4Spring Boot默认UTF-8三者匹配就不会乱。日志问题很容易被忽略。默认情况下Spring Boot自带logback的日志文件没有大小限制和生产级别的滚动策略跑个把月磁盘就可能被打满。建议在生产环境里把日志配置换成下面这种滚动策略logging: file: name: /home/deploy/apps/logs/app.log logback: rollingpolicy: max-file-size: 50MB max-history: 7 total-size-cap: 500MB用这套配置单个日志文件达到50MB就自动切分最多保留7个历史文件总大小不超过500MB磁盘永远不会被日志写爆。还有一个常用命令快速看后端日志里的异常信息grep -n ERROR\|Exception /home/deploy/apps/logs/app.log | tail -n 50最后再分享一个我个人的操作习惯每次发布新版本之后我会把这次改动的关键信息记录到一个发布文档里包括新增了哪些接口、改了哪些配置、动过哪些表结构。这个文档在新版本出问题的时候特别有用——你可以快速判断哪些改动可能与线上故障相关而不是对着整个代码库大海捞针。这一课的内容实操占了大头。你如果把自己手头的项目从本地搬到了云服务器体验过一次完整的部署流程那你对Java全栈的理解会比刷十套面试题都深刻。对我来说部署上线这件事最迷人的地方在于它把你在开发时做的每一个技术决策都变成了可以被测量、被验证的真实结果。本地随便跑通的代码到了线上才会暴露真正的成色而这个“暴露”的过程恰恰就是全栈工程师成长最快的地方。