AI 爆款文章创作器部署上线,走的是「后端 jar 直跑 + 前端 Nginx 托管 + SSE 接口单独反代」这条路,完整流程共八步:环境准备、后端打包、生产配置、启动服务、前端构建、Nginx 接入、SSE 验证、监控与回滚。这套流程同时跑通了「企业级 AI 网关」项目,两者共用同一套部署骨架,区别只在网关多了一层多模型路由配置。下面按实际踩坑顺序把每一步的操作和原因写清楚。
一、部署前先想清楚三件事
动手部署前,先定三件事:部署形态、网络规划、配置管理。这三点没定,后面每一步都会返工。
1.1 部署形态选型
| 部署方式 | 环境一致性 | 发布速度 | 回滚难度 | 适用阶段 |
|---|---|---|---|---|
| jar + nohup 直跑 | 依赖手动装齐 | 快,替换 jar 即可 | 低,留旧包即回滚 | 测试机、小流量上线 |
| systemd 托管 | 同左,多了开机自启 | 中 | 低 | 单机长期运行 |
| Docker Compose | 镜像即环境 | 中,镜像构建占时间 | 中,需版本化镜像 | 生产首选 |
首版上线我选了 systemd 托管后端,原因是 SSE 长连接场景下进程管理必须稳,nohup 的进程在服务器重启后不会自动拉起,而 systemd 的 Restart=always 能兜住偶发崩溃。后续切到 Docker Compose 是另一个故事。
1.2 端口与网络规划
对外只暴露 80/443,后端 8080 端口只监听内网。Nginx 负责把 /api 转到后端,把 /sse 按流式规则单独转,/ 直接服务前端 dist 静态文件。MySQL 只绑 127.0.0.1,Redis 设密码并禁止外网访问。这样安全组只需要开 80、443、22 三个端口。
二、后端部署四步走
2.1 打包
用 Maven 打可执行 jar,跳过测试减少构建时间:
mvn clean package -DskipTests -Dfile.encoding=UTF-8
ls -lh target/ai-writer-backend-*.jar
多阶段生成文章涉及异步任务和 SSE 推送,jar 里必须确认引入 spring-boot-starter-webflux 或 spring-boot-starter-web 的异步支持,否则 SseEmitter 启动即报错。
2.2 生产配置外置
把 application-prod.yml 单独维护,数据库密码、Redis 密码、大模型 API Key 全部从环境变量注入,不写进文件。多阶段生成里每一阶段调哪个模型、温度参数、超时秒数,也都放配置:
server:
port: 8080
compression:
enabled: true
mime-types: application/json,text/html
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/ai_writer?useSSL=false&serverTimezone=Asia/Shanghai
username: ${DB_USER}
password: ${DB_PASSWORD}
data:
redis:
host: 127.0.0.1
password: ${REDIS_PASSWORD}
ai:
writer:
llm:
api-key: ${LLM_API_KEY}
base-url: ${LLM_BASE_URL}
stages:
topic-timeout-ms: 30000
draft-timeout-ms: 60000
2.3 上传并启动
scp target/ai-writer-backend-*.jar deploy@服务器:/opt/ai-writer/backend/app.jar
systemd 服务文件 /etc/systemd/system/ai-writer.service:
[Unit]
Description=AI Writer Backend
After=network.target mysqld.service redis.service
[Service]
User=deploy
WorkingDirectory=/opt/ai-writer/backend
Environment=SPRING_PROFILES_ACTIVE=prod
Environment=DB_USER=writer
Environment=DB_PASSWORD=xxx
Environment=LLM_API_KEY=sk-xxx
ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar app.jar
SuccessExitStatus=143
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ai-writer
sudo systemctl status ai-writer
SuccessExitStatus=143 不能漏,否则 systemd stop 时把正常关闭当成异常退出。
2.4 接口自检
curl -s http://127.0.0.1:8080/actuator/health | jq .
三、前端部署:构建到托管
前端是 Vue 3 + Vite。生产构建关闭 sourcemap,减小体积:
npm ci
npm run build
ls dist/
dist 整个目录上传到 /var/www/ai-writer/dist。前端接口地址通过 Vite 的 VITE_API_BASE 环境变量在构建时注入,指向同域 /api,由 Nginx 反代,规避跨域。
四、Nginx 接入:静态、API、SSE 三块分开配
SSE 是这篇文章创作器交互的核心——前端选主题、点生成,后端按「选题→大纲→初稿→润色」多阶段流式返回。Nginx 若不特殊处理,SSE 会被缓冲成块状输出,体验直接崩掉。
server {
listen 80;
server_name writer.example.com;
root /var/www/ai-writer/dist;
index 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;
}
location /sse/ {
proxy_pass http://127.0.0.1:8080/sse/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no;
gzip off;
chunked_transfer_encoding on;
}
}
两个关键点:/sse/ 的 proxy_pass 结尾带斜杠,路径前缀会被剥掉,后端收到 /sse/... 去掉 /sse 后的真实路径;proxy_buffering off 只作用在 SSE 的 location 里,普通 API 保持默认缓冲,性能不受影响。
4.1 前端 EventSource 与部署形态的配合
前端用 EventSource 订阅生成流,部署时有一个容易忽略的点:EventSource 的地址必须走 /sse/ 这个路径,不能直连后端 8080,否则 Nginx 的负载、SSL、日志全被绕过。前端只开一个连接,后端按多阶段生成流程往同一条流里推 event: stage 事件,前端按阶段渲染状态。上线后最常出现的问题是浏览器对 SSE 长连接的心跳——Nginx 的 proxy_read_timeout 3600s 已经兜底,但后端最好每 15 秒推一条 event: heartbeat,防止中间任何一层空闲超时把流掐断。
const es = new EventSource('/sse/generate?topic=' + encodeURIComponent(topic));
es.addEventListener('stage', (e) => renderStage(JSON.parse(e.data)));
es.onerror = () => { /* 断线自动重连,EventSource 默认行为 */ };
五、部署踩坑记录
这几条坑在测试环境一次都没踩到,全部是在真机部署时才暴露的。
- 连接串里漏了时区参数:MySQL 报
The server time zone value 'CST' is unrecognized,application-prod.yml加serverTimezone=Asia/Shanghai解决; - jar 权限不足:deploy 用户启动时读取不到 API Key 环境变量,因为
.env文件权限是 644 且属主是 root,改成 600 并 chown; - SSE 被 CDN 缓存:如果前面还套了 CDN,必须在源站响应里带
Cache-Control: no-cache,否则中间层把流式响应当普通响应缓存,用户拿到的是截断内容; - 多阶段生成接口超时:网关层默认
connect_timeout是 60 秒,而”初稿生成”阶段在高峰期可能跑 90 秒,Nginx 的proxy_connect_timeout 300s与后端 WebClient 的超时参数要统一。
六、上线验证清单
按下面的顺序跑一遍,任何一步异常都停:
nginx -t && nginx -s reload,确认配置语法通过;curl -s http://127.0.0.1:8080/actuator/health,后端健康检查返回 UP;- 浏览器打开首页,确认 SPA 路由刷新不 404;
- 发起一次真实生成,
curl -N http://127.0.0.1/sse/generate?topic=xxx,确认内容按字符流式吐出; - 停掉后端进程,确认 systemd 5 秒内自动拉起,SSE 连接重连成功。
验证通过后,把旧 jar 备份到 /opt/ai-writer/backup/app-$(date +%s).jar。一旦新版本出问题,替换回去重启即可,这一步让回滚成本压到两分钟内。
七、同一套骨架复用给企业级 AI 网关
这套部署流程后来直接复用到「企业级 AI 网关」项目上。网关用 Spring Boot + Spring AI 接入多模型,对外以 /v1/chat/completions 形式提供 SSE 对话流。部署时只多改了两处:网关的 location 换成 /v1/,同样开 proxy_buffering off;多模型的路由 key 从环境变量注入,每接一个模型加一组变量,不碰代码。前面验证清单里的第 4 步在网关侧验证的就是对话流按 token 逐字吐出。两套系统共用 systemd 服务模板和回滚脚本,新项目上线只需要复制目录、改服务名。
八、上线后的日常运维
进程层面交给 systemd,日志用 journalctl -u ai-writer -f 盯;应用日志按天滚动,保留 7 天,多阶段生成的每个阶段耗时打点,出现生成超时能在日志里定位到具体是哪一阶段调模型卡住。Redis 的过期键数量、MySQL 慢查询两张表每周看一次,这两项是文章创作这类写多读少场景最容易出问题的位置。
常见问题(FAQ)
Q1:刷新页面 404 是什么原因?
前端 location 少了 try_files $uri $uri/ /index.html,Vue Router 的 history 路由找不到物理文件。
Q2:SSE 内容一卡一卡的是为什么?
Nginx 默认缓冲了流式响应。给 SSE 的 location 加 proxy_buffering off 和 X-Accel-Buffering no 即可。
Q3:后端一直重启是哪里没配好?
先看 systemd 日志 journalctl -u ai-writer -f,多半是数据库连接串或环境变量没注入导致启动失败。