
1. 部署前的整体规划与技术选型1.1 这个考试网站到底是什么部署要解决哪些问题先交代一下背景。我手头这个项目是一个面向个人的考试练习网站核心功能包括在线答题、错题记录、成绩统计、题目导入导出还有一个简单的后台管理模块。整套东西前期一直在本地开发环境跑Spring Boot 后端 MySQL 数据库 Vue 前端开发模式下一切正常但真要让别人能用浏览器访问就必须走一遍完整的部署流程。部署这件事听起来就是把代码扔到服务器上启动但实际操作起来要处理的问题非常多。我总结下来至少有这几类服务器环境怎么选、数据库怎么迁移、后端打包后如何配置运行参数、前端资源如何交给 Nginx 托管、域名和 HTTPS 证书怎么处理、进程挂了怎么自动恢复、日志怎么看以及最常见的端口占用、内存不足、跨域报错这些幺蛾子。这篇文章就是把当时从零到上线这一整套流程完整记录下来包括每一步的配置文件、踩过的坑、排查问题的思路以及最后上线稳定运行后的维护经验。适合的对象是手里有个人项目想部署上线的开发者尤其是第一次独立完成全栈项目部署的同学。1.2 技术栈与部署方案选择背后的考量这个考试网站的技术栈是前端 Vue 2 后端 Spring Boot 2.x MySQL 5.7 Redis。选这套组合没有特别花哨的理由主要是我个人技术栈里最熟的一套而且无论是开发还是部署资料都非常多遇到问题很容易查到解决方案。部署方案上我没有选择 Docker 容器化而是直接采用服务器裸机部署 Nginx 反向代理的经典方式。原因有两点第一单机部署就一个后端服务加一套数据库用 Docker 反而引入镜像构建、容器网络配置这些额外复杂度第二裸机部署对排查问题更直观systemctl 查看服务状态、直接翻日志文件对新手更友好。服务器配置方面我用的是一台 2 核 4G 的云服务器系统选了 Ubuntu 20.04。4G 内存对 Spring Boot MySQL 完全够用但要注意的是系统本身会占用一部分内存所以启动参数里必须限制 JVM 堆内存后文细说。提示如果你也是个人项目部署建议优先考虑云厂商的轻量应用服务器性价比更高而且自带的防火墙控制台比纯云服务器更方便。2. 服务器环境准备与基础软件安装2.1 系统初始化与安全设置拿到服务器第一件事不是急着装软件而是先做基础的安全和系统配置。我用 SSH 登录后依次做了这几件事更新系统软件源apt update apt upgrade -y创建新的系统用户用于部署项目不使用 root 直接跑服务配置 SSH 密钥登录关闭密码登录可选但推荐修改系统时区为 Asia/Shanghai同步时间在云厂商控制台的安全组里放行 80、443、3306 这几个端口3306 建议后续改成仅内网可访问这里提一下创建用户的细节。虽然个人项目用 root 部署也没人拦你但我建议还是单独建一个用户比如叫 deploy然后用sudo授权。养成这个习惯的好处是万一服务被攻击攻击者拿到的权限不是 root 级别的而且日常维护操作都在普通用户下执行误操作删除文件的风险也小一些。# 创建用户并加入 sudo 组 sudo adduser deploy sudo usermod -aG sudo deploy # 切换用户验证 sudo 权限 su - deploy sudo whoami # 输出 root 即正常时区设置容易忽略但很影响后续看日志时的体验。如果服务器是 UTC 时区你排查问题时会发现日志时间和本地时间对不上非常别扭。sudo timedatectl set-timezone Asia/Shanghai2.2 JDK、MySQL、Redis、Nginx 的安装与配置环境依赖一共四样JDK、MySQL、Redis、Nginx。我的项目后端基于 Spring Boot 2.x运行时要求 Java 8 以上这里直接装 OpenJDK 11稳定而且兼容性好。# 安装 OpenJDK 11 sudo apt install openjdk-11-jdk -y java -version # 验证安装结果MySQL 的安装建议直接用 apt 源里的版本Ubuntu 20.04 默认源带的是 MySQL 8.0。如果你本地开发用的是 5.7部署时要注意字符集配置。我统一在部署环境设置成 UTF-8避免中文乱码。# 安装 MySQL sudo apt install mysql-server -y # 启动服务并设置为开机自启 sudo systemctl start mysql sudo systemctl enable mysql这里有一个非常关键的步骤MySQL 安装完成后root 账号默认使用 auth_socket 插件认证也就是说只有在系统 root 用户下才能直接登录。这会导致你从 Java 程序里无法用 root 密码连接数据库。解决办法是创建一个专用的数据库账号并授权给目标数据库。-- 使用 sudo mysql 进入 MySQL 命令行 sudo mysql -- 创建数据库账号 CREATE USER exam_userlocalhost IDENTIFIED BY 这里填强密码; GRANT ALL PRIVILEGES ON exam_db.* TO exam_userlocalhost; FLUSH PRIVILEGES;Redis 安装同样简单但要注意配置项。Redis 默认监听回环地址 127.0.0.1这是安全的默认配置不需要改。考试网站里的验证码存储、在线用户状态都用到了 Redis如果 Redis 未启动后端服务会出现连接超时排查时需要留意启动顺序。sudo apt install redis-server -y sudo systemctl start redis sudo systemctl enable redisNginx 是整个部署里的关键角色。因为前端打包后是纯静态文件需要 Nginx 托管后端接口则通过 Nginx 反向代理转发到 Spring Boot 的端口。sudo apt install nginx -y sudo systemctl start nginx sudo systemctl enable nginx注意这里所有服务我都是用 systemctl 启停的。部署阶段我用 systemctl 管理 MySQL、Redis、Nginx 这三个基础组件后端 Java 服务我也是写 systemd 服务文件来管理。统一用 systemctl开发环境那套手工nohup java -jar的方式就不用在线上用了日志管理和进程守护都规范很多。3. 数据库准备与初始化配置3.1 本地数据库迁移到服务器本地开发环境里我已经有了一份完整的考试数据包括题库、用户表、考试记录等。要迁移到服务器最直接的方式是通过 mysqldump 导出再导入。先在本地执行导出mysqldump -u root -p exam_db exam_db_backup.sql然后通过 scp 拷贝到服务器scp exam_db_backup.sql deploy服务器IP:/home/deploy/登录服务器后导入mysql -u exam_user -p exam_db exam_db_backup.sql这个过程看起来简单但有几个坑值得提醒。第一是编码问题导出和导入时都要显式指定字符集为 UTF-8第二是大小限制如果数据库文件很大直接用重定向导入可能因为内存不足失败建议分开导入表结构再导入数据第三是权限问题确保 exam_user 对 exam_db 库有完整操作权限。# 指定字符集导出 mysqldump -u root -p --default-character-setutf8mb4 exam_db exam_db_backup.sql # 指定字符集导入 mysql -u exam_user -p --default-character-setutf8mb4 exam_db exam_db_backup.sql3.2 连接数据库的配置细节Spring Boot 的数据库配置一般在application.yml或application.properties里但这里要重点强调一个经验不要把生产环境的数据库密码直接写死在配置文件里然后把这个文件上传到服务器。因为代码仓库、前后端联调、备份文件里都可能残留配置时间一长就忘了哪些地方暴露过密码。我推荐的做法是使用环境变量注入spring: datasource: url: jdbc:mysql://127.0.0.1:3306/exam_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: ${DB_USERNAME} password: ${DB_PASSWORD}启动服务时通过 systemd 服务文件把这些环境变量传进去。这样数据库凭据就只存在于服务器上的服务配置里而不是混在代码包中。还有一个细节是 JDBC 连接串里的serverTimezoneAsia/Shanghai这个不加的话如果 MySQL 和 JVM 的时区不一致Java 程序查询时间字段时会发现时间差了好几个小时。4. 后端服务的打包与部署实操4.1 Maven 打包的两种方式与选择Spring Boot 项目常规打包就是 Maven 的mvn clean package会在 target 目录生成一个可执行 jar 包。因为项目自带 Maven Wrapper本地执行./mvnw clean package -DskipTests就行不用额外安装 Maven。打包结果的 jar 包大小大概 80MB 左右这里面包含了内嵌的 Tomcat 服务器和所有依赖库所以可以直接跑java -jar启动。一个值得注意的问题如果你在本地打包后上传到服务器因为本地系统和云端都是 LinuxJDK 版本差一个版本运行的时候可能会遇到UnsupportedClassVersionError。当时我的解决办法很简单本地打包时指定 JDK 11服务器也装 JDK 11两边一致即可。提示如果服务器内存只给 Spring Boot 分配 512MB建议刚装完环境就先做个 Java 版本验证别等到部署完再排查这种基础问题。4.2 编写 systemd 服务文件实现进程守护后端部署的核心环节是让 Spring Boot 进程以受控的方式运行在服务器上。直接nohup java -jar exam.jar 的方式当然能启动但缺点很明显进程重启没有自动恢复、日志管理混乱、想查看运行状态还得自己想办法。我采用的是 systemd 服务方式创建一个服务文件[Unit] DescriptionExam Website Backend Service Afternetwork.target mysql.service redis.service [Service] Userdeploy WorkingDirectory/home/deploy/app EnvironmentDB_USERNAMEexam_user EnvironmentDB_PASSWORD你的数据库密码 EnvironmentJAVA_OPTS-Xms256m -Xmx512m -XX:UseG1GC ExecStart/usr/bin/java $JAVA_OPTS -jar /home/deploy/app/exam.jar Restartalways RestartSec10 StandardOutputappend:/home/deploy/app/logs/stdout.log StandardErrorappend:/home/deploy/app/logs/stderr.log [Install] WantedBymulti-user.target这个服务文件里的几个配置比较关键Aftermysql.service redis.service确保数据库和缓存先启动Spring Boot 启动时就能连上数据库Restartalways表示进程任何异常退出都会自动重启这是 Java 进程宕机后的第一道保险RestartSec10让重启有个间隔避免进程崩溃后立刻重启导致 CPU 飙升JAVA_OPTS里的堆内存限制非常关键4G 内存的机器如果不限制堆内存JVM 默认申请物理内存的 1/4也就是 1G加上系统本身占用的余量很小把文件放到/etc/systemd/system/目录下然后执行sudo systemctl daemon-reload再启动服务sudo systemctl daemon-reload sudo systemctl start exam.service sudo systemctl enable exam.service查看服务状态建议直接看日志。我在服务文件里把标准输出和标准错误重定向到了独立日志文件这样journalctl -u exam和直接tail -f logs/stderr.log都可以用看业务报错时 tail 文件更直接。4.3 后端接口的验证与常见启动问题服务启动后先验证一下接口是否正常curl http://127.0.0.1:8080/api/health这一步能通说明后端服务本身没问题。如果访问不通就去看 stderr.log 的原因。我踩过最典型的两个坑这里先剧透一个端口被占用。因为本机可能装了其他服务占了 8080解决办法是修改 Spring Boot 的端口配置或者用lsof -i:8080找到占用进程处理掉。第二个常见坑是 MySQL 连接失败日志里会明确提示Access denied for user或Communications link failure排查方向从数据库账号密码、网络连通性、连接串配置三个维度入手。后端验证通过后下一步就是把前端部署上去再用 Nginx 完成真正的对外访问。5. 前端打包与 Nginx 配置详解5.1 前端项目构建与打包前端项目是 Vue 2 Element UI本地运行使用的代理配置在vue.config.js里开发环境把/api开头的请求代理到localhost:8080。部署到线上时这个代理配置只服务于本地开发真正的请求转发要在 Nginx 层完成。打包命令npm run build构建完成后生成dist目录。这个目录就是最终要上传到服务器的静态资源。我把 dist 目录下的内容上传到服务器的/home/deploy/www目录。上传方式用 scp 即可scp -r dist/* deploy服务器IP:/home/deploy/www/注意这里不要直接把 dist 目录整个传过去而是传 dist 下的内容这样 Nginx 的 root 配置就能直接指到/home/deploy/www。5.2 Nginx 配置的完整结构与关键参数Nginx 配置文件我用的是/etc/nginx/sites-available/exam.conf这种标准方式。相比于直接写在nginx.conf里site 配置更清晰而且方便通过软链接切换启用状态。server { listen 80; server_name exam.example.com; gzip on; gzip_types text/plain application/javascript text/css application/json image/svgxml; gzip_min_length 1k; root /home/deploy/www; index index.html; # 前端路由支持Vue Router 的 history 模式 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存策略 location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico)$ { expires 7d; } # 后端 API 反向代理 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; } }几个配置项的用意说明一下try_files $uri $uri/ /index.html是前端 history 路由模式的标准配置。如果用户直接访问/exam/123这个路径服务器上并没有这个真实文件就会回退到 index.html由 Vue Router 接管页面。静态资源缓存配置是为了提高访问速度。图片、JS、CSS 这类文件设置 7 天缓存浏览器再次访问直接走本地缓存减轻服务器压力。要注意的是如果你的网站发布新版本后还看到旧样式通常就是这里缓存没更新需要手动处理。反向代理配置里proxy_set_header几个请求头是必须的尤其是X-Forwarded-For后端程序才能拿到用户真实 IP。有些统计功能、登录 IP 限制就靠这些请求头。配置写好后启用站点并重载 Nginxsudo ln -s /etc/nginx/sites-available/exam.conf /etc/nginx/sites-enabled/ sudo nginx -t # 测试配置有没有语法错误 sudo systemctl reload nginxnginx -t这个测试命令必须要执行如果配置有语法错误reload 会失败更差的情况是 Nginx 被中断临时不可用。5.3 前端资源更新时如何优雅替换文件考试网站上线后题库和页面功能会频繁更新这就涉及前端重新打包后如何替换服务器上的旧文件。这里分享一个我后来固定下来的流程。先在家目录建一个 release 目录每次新的 dist 内容先传到这个目录然后通过 symlink 切换# 第一次部署 mv /home/deploy/www /home/deploy/www_bak ln -s /home/deploy/release_20240601 /home/deploy/www # 后续更新 mv /home/deploy/www /home/deploy/www_old ln -s /home/deploy/release_20240610 /home/deploy/www这样做的价值在于每次发布都保留了上一个版本的文件如果新版本出现问题一条命令就能快速回滚。对于没有上 CI/CD 系统的个人项目这套手动发布方案已经成为我维护考试网站时最可靠的通用流程。6. HTTPS 证书配置与访问入口完善6.1 为什么必须配置 HTTPS考试网站涉及用户登录、答题记录、成绩查询这类用户数据在传输过程中如果走明文 HTTP等于在网络上裸奔。尤其是用户可能在公共 WiFi 下访问数据被抓包完全有可能泄露用户密码和考试内容。现在配置 HTTPS 已经非常简单不需要自建证书体系有免费的证书申请方案可用。我用的是 Lets Encrypt 的证书配合 certbot 自动续期工具。6.2 证书申请与 Nginx 自动化配置安装 certbotsudo apt install certbot python3-certbot-nginx -y执行自动申请sudo certbot --nginx -d exam.example.comcertbot 会检测到已经存在的 Nginx 配置自动修改配置文件并添加证书配置。整个过程大概是验证域名所有权、下载证书、修改 Nginx 配置启用 HTTPS、设置自动跳转。配置完成后原来的 80 端口会自动跳转到 443。此时访问https://exam.example.com就能正常打开考试网站了。这里重点讲一下证书自动续期的验证方法。Lets Encrypt 证书有效期是 90 天certbot 安装时会自动添加 systemd timer。你可以用下面的命令确认续期任务存在sudo systemctl list-timers | grep certbot如果输出里有 certbot.timer就说明自动续期已经生效。如果不放心可以手动跑一次续期命令测试sudo certbot renew --dry-run这条命令模拟续期流程能确认证书在到期前会正常刷新。注意HTTPS 配置完成后要在后端服务里把 Cookie 的 Secure 属性打开否则浏览器可能拒绝在 HTTPS 环境下保存登录 Cookie。Spring Boot 配置server.servlet.session.cookie.securetrue即可。7. 上线后的功能验证与监控体系7.1 完整链路的功能回归测试部署完成不代表测试就可以省略。考试网站的核心链路包括注册登录、答题流程、成绩提交、历史记录查询、后台管理操作这几个环节上线后至少完整走一遍。我当时的做法是这样的测试注册新账号确认邮件或短信验证码能正常下发这里用不到短信服务邮件前面有配置登录后进入考试列表随机选择一套试卷进入答题界面提交试卷后确认成绩写入数据库再回到历史记录页面确认能看到本次成绩管理员登录后台确认题目管理、用户管理功能可操作测试断网和弱网场景确认前端有错误提示而不是白屏这个过程中发现了一个真实问题上传题目时如果包含大量图片请求体超过 Spring 默认的 1MB 限制会报 413 错误。解决办法是在 Spring Boot 配置中调大 multipart 限制spring: servlet: multipart: max-file-size: 20MB max-request-size: 30MB同时 Nginx 层也要同步调整client_max_body_size 30m;这个双层限制的问题特别容易踩坑。只改 Spring 不改 NginxNginx 会先拦截只改 Nginx 不改 Spring后端会报错。两边要一致。7.2 日志监控与资源监控的结合后端日志排查问题时非常关键。Spring Boot 应用在部署后不像本地开发环境能直接看到控制台输出必须把日志配置好。我建议引入 logback 的滚动日志配置按天生成日志文件保留 7 天appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file/home/deploy/app/logs/exam.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern/home/deploy/app/logs/exam.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory7/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/pattern /encoder /appender这样即便程序运行几个月日志文件也不至于把磁盘塞爆而且出问题时能按天定位时间窗口。资源监控方面我这个项目的经验是不需要上 Prometheus、Grafana 这类重型监控方案2G 内存的服务器上跑这些组件反而拖累主业务。直接用基础的top和df -h定期看内存、磁盘即可验证完部署效果后确认基础可用性就达到目的了。如果项目后续变复杂再考虑引入轻量级监控方案。8. 常见问题与排查技巧实录8.1 冷启动与端口冲突类问题问题1服务启动后 CPU 居高不下。原因是 JVM 参数没限制好G1 垃圾回收器在默认配置下运行不理想。调整后的参数是-XX:UseG1GC -Xms256m -Xmx512m实测启动完成后 CPU 稳定在 2% 左右。问题28080 端口被占用。排查命令很基础lsof -i:8080找到占用进程后用kill PID处理然后重启自己的服务。这个坑在上线初期出现过一次因为系统自带的某个服务占用了这个端口改成自定义端口后就没再遇到。问题3前端页面能打开接口全部 404。这个很典型。先确认接口路径是否带 /api 前缀。本地开发时我通过代理去掉前缀请求后端但线上 Nginx 配置里 proxy_pass 保留了这个前缀如果前端请求路径是/api/login而后端本身定义的路径就是/login就会 404。解决办法是后端项目统一设置 context-path或者把 Nginx 的转发规则改成去掉前缀的写法location /api/ { proxy_pass http://127.0.0.1:8080/; }前面那个proxy_pass后面带不带斜杠差异很大。带斜杠表示把/api前缀丢弃后转发不带斜杠则保留完整路径。我在配置时选择保留前缀的方案后端接口路径统一以/api开头这样一致性好记不需要在两侧玩差异化匹配。8.2 数据库连接与跨域问题数据库连接问题也是高频事故。我当时有一次 MySQL 服务掉线后端日志里刷了大量Communications link failure错误。排查思路是确认 MySQL 是否在运行systemctl status mysql确认网络连通性ping或telnet 127.0.0.1 3306确认连接账号密码是否有权限变更确认连接串的字符编码和时区配置如果这些都没问题但连接还是频繁断开排查数据库连接池配置。Spring Boot 2.x 默认使用 HikariCP它的max-lifetime默认是 30 分钟而 MySQL 服务端的wait_timeout默认是 8 小时这两者之间的配合情况要看具体环境如果拿不准可以用短一点的 connection 参数测试。跨域问题在上线后其实不太会出现因为前后端已经通过 Nginx 同域部署了浏览器判断同源是看域名加端口没有跨域问题。但如果此时后台接口报 403大概率是 Nginx 配置的proxy_set_header缺少了Host或Origin这些头导致后端拒绝请求。8.3 一个让我排查了两天的诡异问题最后分享一个困扰我最久的问题前端偶尔会出现登录状态失效刷新后又恢复了。我先以为是 Redis 的 session 过期配置问题改了失效时间之后仍然偶发。后来又怀疑是 Nginx 的缓存搞的鬼测试后也不是。最后排查到问题出在后端项目的spring.session.timeout使用方式和 Nginx 对静态资源的expires设置上。其实是我把前端index.html也纳入了缓存策略导致用户在打开页面时拿到了旧的 HTML 内容而旧版本的 JS 引用的接口地址已经变了。新接口要求带某种请求头旧代码没带所以权限校验失败看起来像是登录失效了。刷新后拿到了新的 HTML 和新 JS 文件请求头带上了又恢复正常。这个问题的根源就是发布新版本时没有让index.html跳过缓存。正确的配置是只让带哈希后缀的静态资源走缓存而index.html永不缓存location /index.html { add_header Cache-Control no-cache, no-store, must-revalidate; } location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico)$ { expires 7d; }那次经历之后我所有的静态资源文件名都带上了内容哈希这样每个版本的文件名不同缓存策略就不会和新版本发布产生冲突。Vue CLI 默认打包就会给文件名加 hash所以问题只出在部署配置上。考试网站后续的所有更新都遵循这个规则再没出过类似问题。9. 经验沉淀与后续扩展方向整套部署流程走下来给我最大的感受是部署一个个人项目的过程本质上是在把一个在理想环境下开发的软件放到一个不够理想的真实环境里做边界条件验证。数据库连不上怎么处理、缓存意外掉了怎么恢复、进程被系统杀了会不会自愈、日志会不会把磁盘打满、更新版本时怎么保证平滑这些问题只有在线上的真实压力下才会暴露出来。对还没部署过个人项目的同学我建议按这个顺序推进先在本地把 打包-部署-验证 的流程完整跑一遍哪怕用的是虚拟机和本地 DNS 解析然后再迁移到云服务器毕竟云上多了安全组、防火墙这些环节最后再考虑自动化部署、监控告警这些锦上添花的能力。每一步都等前一步稳定了再走排起错来心里有底。考试网站这个项目目前已经稳定运行了一段时间后续我打算在自动化部署上再往前走一步。像现在这样手工打包上传虽然已经形成固定流程但每次都要在本地执行 Maven 打包再上传到服务器再软链切换版本。如果考虑引入简单的构建脚本把远程更新变成一个命令操作步骤少了不说也能减少手工操作带来的失误概率。如果你也是个人项目的部署我那套 symlink 切换版本的方式可以先抄起来成本极低但回滚能力非常实用。