
开发环境里跑得飞快的代码一部署到生产服务器就各种翻车这种经历但凡干过部署的人都懂。《看潮企业管理软件》做到第22个环节“项目部署”时我心里清楚真正的考验不是功能写得怎么样而是这套系统能不能在另一台干净的机器上稳定地跑起来、跑长久。看潮这个项目我用的是典型的前后端分离结构前端一个独立工程后端一个Spring Boot服务数据库和缓存各自独立。一到部署阶段环境差异、路径问题、端口冲突、内存不足、字符集乱码……所有这些开发时根本不会出现的坑全冒出来了。这篇就完整记录一下我把看潮从一台开发机搬到生产服务器的全过程包括环境规划、中间件安装、前后端部署、数据库备份和故障排查照着做基本能少走一半弯路。1. 部署前先搞清楚这套系统在生产环境到底长什么样1.1 先从架构说起前后端分离是部署的起点看潮这套企业管理软件的功能模块我大致拆成了客户管理、订单流转、库存台账和审批流程几个部分。放在开发环境里前端有Node的开发服务器后端有IDE直接启动数据库连的是本机MySQL一切都很“顺”。可生产环境没有这些便利条件必须先把系统的运行形态定下来。生产环境的运行形态是前端打包成纯静态文件由Nginx负责托管后端打成一个可执行的jar包由守护进程托管MySQL和Redis各自独立运行。三者之间的调用关系是浏览器先请求NginxNginx把静态资源直接返回给浏览器浏览器再通过接口域名去请求后端APINginx再把符合规则的API请求反向代理给Spring Boot服务。前后端之间不再像开发时那样靠Node代理转发而是完全走真实网络请求。这个架构确定下来之后很多问题就提前暴露了最典型的是跨域。开发阶段Vite或Webpack的proxy配置帮忙把跨域问题“藏”起来了生产环境一旦前后端不同源浏览器就会报CORS错误。我在部署前就专门检查了后端是否配置了跨域过滤器以及前端接口请求的baseURL到底是相对路径还是写死的开发地址这两个地方出错页面能打开但登录永远登不进去。1.2 部署形态选型直接跑服务还是容器化看潮这个项目部署时我纠结过两套方案传统部署和Docker容器化。传统部署就是直接在服务器上装JDK、MySQL、Redis、Nginx每个服务各自安装管理逻辑简单出了问题也容易定位。容器化则是把每个服务打成镜像用Docker Compose统一编排启动环境一致性更好换机器的时候一条命令就能拉起一套环境。最终我给看潮选择了传统部署加systemd托管的方式。原因是这套系统是单体架构组件就四个传统部署的维护成本完全可控另外服务器内存只有4G跑容器会额外占用资源对Java应用来说内存本来就紧俏越轻量越稳。但如果你的服务器是8G以上内存或者后续要频繁迁移环境我建议直接用Docker Compose镜像构建好之后部署速度会快很多而且不用在每台新机器上重新折腾一遍中间件安装。无论选哪种方式有一件事是一致的部署脚本和文档必须沉淀下来。我当时把所有命令整理成了一个部署手册后来系统要迁移到另一台服务器照着手册一步步执行半小时就恢复了一套可用环境这个习惯帮我省了太多事。2. 服务器初始化该装的软件一个都不能少2.1 服务器与基础配置从裸机到可用状态我给看潮准备了一台2核4G的云服务器操作系统选的Ubuntu 22.04磁盘40G。这个配置跑一套中小型管理系统刚好够用再低的配置跑起来会经常因为内存不足导致服务被系统杀掉。拿到一台全新的服务器第一件事不是装软件而是把系统基础环境配好。我一般按这个顺序来更新系统软件源把系统补丁打全。创建专用部署账号生产环境不建议直接拿root跑应用。配置SSH密钥登录关闭密码登录降低暴力破解风险。调整系统时区统一为Asia/Shanghai避免日志时间对不上。优化内核参数主要是文件句柄数量和TCP连接复用。创建部署账号和设置时区的命令如下# 创建部署用户 useradd -m -s /bin/bash deploy # 给部署用户sudo权限按需 usermod -aG sudo deploy # 设置系统时区 timedatectl set-timezone Asia/Shanghai关于文件句柄和连接数这块很多新手会忽略。Spring Boot应用在高并发下会大量创建网络连接和文件句柄如果系统默认限制太低服务跑着跑着就会报“Too many open files”或连接不上数据库。我部署看潮时在/etc/sysctl.conf里加了这段让系统允许更多连接# 优化网络连接参数 fs.file-max 655350 net.core.somaxconn 1024 net.ipv4.tcp_max_syn_backlog 2048 net.ipv4.ip_local_port_range 1024 65535执行sysctl -p让配置生效然后把部署用户的进程限制也调高在/etc/security/limits.conf里加一行deploy soft nofile 65535和deploy hard nofile 65535。这些参数平时不起眼但关键时刻能救命。2.2 中间件安装JDK、MySQL、Redis、Nginx看潮后端用的是Java 17所以我先装了OpenJDK 17。这里强调一点生产环境JDK版本必须和开发环境完全一致哪怕是小版本差异也偶尔会出现class版本兼容问题最保险的做法是本地开发用哪个版本服务器就装哪个版本。安装命令apt install openjdk-17-jdk -y java -version然后是MySQL。我用的是MySQL 8.0安装之后有几个配置必须马上处理字符集改成utf8mb4、默认排序规则改成utf8mb4_unicode_ci、时区改成东八区。这些配置在/etc/mysql/mysql.conf.d/mysqld.cnf里加[mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci default-time-zone 08:00 max_connections 500改完重启MySQL服务然后创建看潮的数据库账号和业务库。数据库账号我只给了业务库的权限没给所有库的权限更没允许远程root登录这是数据库安全的基本底线。Redis安装相对简单装完7.0版本后我改了两处配置绑定的监听地址改为127.0.0.1这样只有本机程序能访问设置一个强密码防止服务器被扫到后Redis被攻击。看潮只在后端服务里用到了Redis前端不直接连所以不需要对外开放Redis端口。2.3 安全组与防火墙端口能少开就少开这一点我特别想多说两句。很多同学部署完服务把端口全放开了一扫描全是敞开的入口这是妥妥的靶子。看潮对外只需要一个HTTP端口和一个HTTPS端口最多再加一个SSH管理端口其他端口一律不放通。我用宝塔面板或者云控制台的安全组都是在入口层开白名单SSH只允许自己办公网络的IP访问HTTP和HTTPS设成80和443放开。MySQL的3306、Redis的6379、Spring Boot的8080这些端口全部不允许外部访问。这样即使服务本身有漏洞攻击者也够不到端口。服务器防火墙命令ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp ufw enable把管理端口限制在固定IP上是最好的做法没有固定IP的话也要在安全组里尽量缩小来源范围。3. 后端部署让Spring Boot服务稳定跑起来3.1 配置分离dev和prod一定要分开看潮后端的配置一开始是写在一个application.yml里的数据库地址是localhostRedis没密码日志打印在控制台。这些配置开发时可以生产环境直接炸。所以第一步就是把配置按环境拆开。Spring Boot本身已经支持多环境配置我在src/main/resources下放了三个文件application.yml公共配置、application-dev.yml开发环境、application-prod.yml生产环境。生产配置里指定外部的数据库地址、Redis密码、日志路径里面这些敏感信息不能用默认值启动时也通过环境变量传入避免把密码写死在jar包里面。生产配置的一个关键点是日志路径。开发时日志直接打在控制台生产环境必须记录到文件里方便出问题时排查。我在application-prod.yml里配置了日志滚动策略按天切分保存最近30天单文件超过50MB自动切割。日志级别生产环境调到INFOWeb框架和SQL日志单独控制不然日志文件一天就能撑爆磁盘。3.2 systemd托管别再用nohup了很多老的部署教程会教nohup java -jar xxx.jar 启动这种方式能跑但有几个致命问题服务意外挂掉没人拉它起来服务器重启后服务不会自动启动日志管理也麻烦。部署看潮我用的是Linux自带的systemd服务托管把Spring Boot应用变成系统服务。我在/etc/systemd/system/kanchao.service里写了一个服务文件[Unit] DescriptionKanchao Management System Afternetwork.target mysql.service redis-server.service [Service] Userdeploy WorkingDirectory/opt/kanchao ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /opt/kanchao/kanchao.jar Restartalways RestartSec10 SuccessExitStatus143 LimitNOFILE65535 EnvironmentSPRING_PROFILES_ACTIVEprod [Install] WantedBymulti-user.target这段配置里几个参数值得解释一下。After表示这个服务要在网络、MySQL和Redis启动之后才启动避免后端起来连不上依赖。Restartalways是灵魂配置服务进程异常退出systemd会在10秒后自动拉起它这是保证可用性的最基本手段。SuccessExitStatus143是为了让systemd正确识别Java应用接收SIGTERM后正常退出的状态码否则每次正常停服务systemd都会报错。内存参数我给了512M到1G这个数值要看你服务器的总内存和并发量不能盲目给太高因为还要给MySQL和Redis留空间。配置好后执行systemctl daemon-reload systemctl enable kanchao systemctl start kanchao服务状态可以用systemctl status kanchao看启动日志用journalctl -u kanchao -f实时跟踪。实测下来这套组合非常省心期间MySQL重启过一次服务自己爬起来继续运转我完全没介入。3.3 用Docker时该怎么写Dockerfile如果你的环境选了容器化方案我给看潮也准备了一个Dockerfile做参考。核心思路是多阶段构建第一阶段用Maven镜像编译打包第二阶段只把jar包放进JRE运行镜像里这样最终镜像体积小很多也不会把源码和编译工具带进生产环境。# 第一阶段构建 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /app/target/kanchao.jar ./kanchao.jar EXPOSE 8080 ENV SPRING_PROFILES_ACTIVEprod ENTRYPOINT [java, -jar, kanchao.jar]这里需要注意配置和日志不要写死进镜像用-v参数把宿主机目录挂载进容器比如配置目录/opt/kanchao/config挂到容器的/app/config日志目录同理。这样升级版本只需要替换jar包重新构建镜像配置和数据都留在宿主机上。用Docker部署还有一个好处本地构建和服务器构建环境一致不会再出现“我机器上能跑啊”这种问题。4. 前端部署Nginx下看潮页面的正确打开方式4.1 前端构建与静态资源上传看潮前端我用的Vue 3写的部署第一步是本地构建出静态资源包。构建之前有两处必须检查一是接口请求地址不能写localhost或局域网IP二是路由模式如果用了history模式后面Nginx必须配对应的回退规则。我习惯把接口地址统一放到环境配置文件里构建测试环境时用测试API地址构建生产时注入生产API地址。构建命令很简单npm install npm run build构建产物在dist目录下生成了index.html和一堆带哈希的JS、CSS文件。这些文件我打包上传到服务器的/opt/kanchao/frontend目录上传后检查一下目录所有者是不是deploy用户不然Nginx读取时报权限错误又得排查半天。有一点容易踩坑前端dist目录要放在一个Nginx能访问的路径且路径里最好不要有中文或空格。有一回我把目录命名成了“前端项目”Nginx配置了半天都404最后才发现是文件路径里的特殊字符问题折腾得够呛。4.2 Nginx配置细节history路由、缓存与Gzip看潮的Nginx配置我写在了/etc/nginx/sites-available/kanchao.conf里这个文件包含静态资源服务配置server { listen 80; server_name kanchao.example.com; root /opt/kanchao/frontend; index index.html; gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1024; location / { try_files $uri $uri/ /index.html; } location ~* \.(js|css|png|jpg|svg|woff2?)$ { expires 30d; add_header Cache-Control public, max-age2592000; } }这里最核心的是try_files $uri $uri/ /index.html这一行。因为前端用了history路由用户在浏览器直接访问/orders这样的路径时服务器上并没有这个物理目录如果不回退到index.html就会404。加上这行所有合法路由都会加载前端的入口页面再由前端路由接管。静态资源缓存这块我给带哈希的JS和CSS设置30天的缓存这类文件内容变了文件名就会变不会出现缓存不更新的问题而index.html本身不设缓存或用短缓存因为它的内容会随着构建更新。4.3 反向代理与HTTPS把API指到后端静态文件配置好了还要处理动态请求。看潮后端的接口统一以/api开头所以Nginx里再加一个location规则把这些请求反向代理到Spring Boot服务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; proxy_connect_timeout 5s; proxy_read_timeout 60s; }这里有个容易出错的点proxy_pass http://127.0.0.1:8080/后面这个斜杠非常关键。带斜杠表示把/api前缀去掉把剩下的路径直接转发给后端比如请求/api/auth/login会转发成/auth/login不带斜杠则是把整个原路径传过去。看潮后端接口没有/api前缀所以我用了带斜杠的写法两者搞反就会得到404或接口不匹配的报错。代理建好后我又申请了SSL证书配置HTTPS。证书拿到后Nginx里监听443端口并加载证书server { listen 443 ssl; server_name kanchao.example.com; ssl_certificate /etc/nginx/ssl/kanchao_fullchain.crt; ssl_certificate_key /etc/nginx/ssl/kanchao.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; }然后把80端口的请求全部301跳转到HTTPS保证用户访问的都是加密连接。配置改完执行nginx -t测试语法通过后systemctl reload nginx。5. 数据库与数据安全上线前最容易被忽略的部分5.1 初始化数据字符集时区一个都别错数据库这块我踩过最大的坑是字符集。测试环境MySQL用的默认字符集latin1页面录入中文没问题但导入生产环境的utf8mb4库后表里的中文全变成了问号。所以部署看潮时我规定所有业务表必须显式指定字符集和排序规则比如创建表时带上DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci不能依赖全局默认值。初始化数据我用的是mysqldump导出的SQL文件导入生产库mysql -u kanchao -p kanchao_db /opt/kanchao/sql/init.sql导入之后一定检查三件事数据量是不是和开发环境一致、表结构有没有缺失索引、自增主键有没有异常偏移。我那次导入后密密麻麻检查完才敢启动后端发现少了两张表的唯一索引补上后接口性能才正常。如果项目用了JPA或Hibernate自动建表我建议生产环境还是换成显式的SQL迁移脚本。自动建表有个问题字段更新不可控数据库结构变更不追溯生产环境一旦数据多了drop再重建是灾难性的。用Flyway这类工具管理迁移脚本每次部署前自动执行增量SQL数据表变更全程有记录。5.2 备份脚本定时备份加上恢复演练数据安全这块我部署看潮的时候专门写了一个备份脚本纳入定时任务。脚本核心逻辑是mysqldump导出全部业务库、压缩、按日期命名、只保留最近7天备份。这样的好处是磁盘不会被备份占满同时能快速找到最近任意一天的备份文件。以下是简化后的备份脚本#!/bin/bash BACKUP_DIR/opt/backup/mysql DATE$(date %Y%m%d_%H%M%S) DB_USERkanchao DB_PASSYourStrongPassword mkdir -p $BACKUP_DIR mysqldump -u$DB_USER -p$DB_PASS --single-transaction --routines --triggers kanchao_db | gzip $BACKUP_DIR/kanchao_$DATE.sql.gz find $BACKUP_DIR -type f -name *.sql.gz -mtime 7 -delete脚本放进/etc/cron.d/kanchao_backup任务里每天凌晨3点执行一次。这里用到的--single-transaction参数很重要InnoDB表备份时不会锁表业务在备份期间照常运行数据一致性也能保证。备份脚本写完不算完必须测试恢复。我专门做了一次演练把备份文件解压后导回一个临时库检查关键表数据是否完整然后再确认前端页面能正常登录、能查到数据。没验证过的备份等于没有备份这句话我在部署任何项目时都会强调。6. 上线验证与问题排查实录6.1 上线检查清单按这个顺序过一遍所有服务配置好后我整理了一份上线检查清单每次部署都按这个顺序走一遍基本能覆盖九成的问题。先看后端服务是否存活、有没有异常日志再看前端页面能否打开检查登录接口、数据列表、文件上传、导出等核心功能最后还要验证系统重启后的自恢复能力。具体检查项后端服务状态正常systemctl status kanchao显示active (running)。Spring Boot健康检查接口返回正常数据库和Redis连接都通过。浏览器直接访问域名页面能打开且无控制台报错。登录、查询、新增、编辑、删除、文件上传等核心链路全部走通。后端日志里无ERROR级别日志无数据库超时和连接池耗尽报警。重启MySQL和Redis后后端服务能自动恢复连接。定时备份任务已配置并实际执行过一次。这些检查全部通过我才会把域名正式切到新配置上否则宁可先维护维护期页面也不让用户看到一个半残的系统。6.2 经典故障排查速查表部署过程中遇到问题不要慌大多数故障都有固定的排查路径。我整理了一张速查表都是我实际碰到过并验证过的现象可能原因排查方式页面打不开Nginx没启动、端口被占用、安全组未放行nginx -t、ss -lntp、检查云控制台安全组刷新页面404history路由缺少回退规则检查try_files配置是否包含/index.htmlAPI请求502后端服务挂了、代理地址写错systemctl status kanchao、curl http://127.0.0.1:8080登录报错500数据库连接失败、Redis连接失败查看后端日志、测试3306和6379连通性接口返回慢数据库缺索引、连接池太小SHOW PROCESSLIST、检查慢查询日志中文乱码数据库字符集配置错误检查character-set-server和表字符集上传文件失败目录权限不足、磁盘已满df -h、检查上传目录写权限服务自动挂掉内存不足触发OOM Killerdmesg6.3 我踩过的几个坑与最终处理部署看潮这个项目时最让我印象深刻的是两个问题。第一个是Nginx代理丢前缀导致的接口404我配置proxy_pass时没加末尾斜杠导致请求路径全带上了/api后端接口匹配不上整个系统所有页面数据都加载不出来。这个问题花了我一个多小时当时没想到是这种低级配置差异引起的后来在文档里专门标注了斜杠的两种用法才算放下心。第二个是内存不足导致服务频繁被杀。服务器4G内存MySQL占1GRedis占200M后端我给JVM分配了2G结果一启动系统就出现内存抖动时不时冒出OOM Killer的日志。后来我把JVM堆从2G降到1G又给系统加了2G swap分区服务才稳定下来。这也提醒我部署前必须评估整机内存的分配不能只看单个服务的参数。还有一个小细节看潮项目的上传目录一开始放在了/tmp下面服务器重启后文件全没了客户的附件全部丢失。后来我把上传路径独立挂载到数据盘并在Nginx里配置了对应目录的访问规则这个问题彻底解决。任何需要长期保存的数据都不要放在临时目录里这个教训成本不低。这次部署经历让我养成了一个习惯每次部署完都会写一份部署记录把环境配置、踩坑过程、最终结论都记下来。下一次做类似项目直接翻出旧记录照着避坑效率比重新摸索快得多。部署这件事看上去只是把代码放到服务器上但只要把架构、配置、数据、安全这几件事理顺才能真正让一个项目在产品环境里站住脚。