跳到主要内容

3 篇博文 含有标签「前端部署」

查看所有标签

本地能调通线上 404?Vite Proxy 不参与生产,需要 Nginx 反代

· 阅读需 5 分钟

Vite 项目本地开发接口全通,npm run build 部署到服务器后,页面能打开,但所有 /api 请求返回 404——或者更隐蔽:返回的是 index.html,前端把 HTML 当 JSON 解析直接报错。

在为客户构建 AI Agent SaaS 平台时遇到此问题,前后端分离部署,记录根因与解法。

TL;DR​

vite.config.ts 里的 server.proxy 只属于 dev server。 npm run build 产出纯静态文件,没有任何代理层;生产环境由 Nginx 接住 /api:

location /api {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

加在 SPA fallback(try_files ... /index.html)能命中的范围之外,nginx -t 校验后 nginx -s reload。

问题现象​

两种表现,同一个根因:

表现一:直接 404

GET https://example.com/api/agents  → 404 Not Found

表现二:返回 index.html(更隐蔽)

GET /api/agents  → 200 OK,响应体却是 <!DOCTYPE html>...
前端 JSON.parse 失败:Unexpected token '<'

第二种是 SPA fallback 吃掉了请求:Nginx 找不到 /api/agents 对应的静态文件,try_files $uri /index.html 把它兜底到了前端入口页。状态码还是 200,等前端解析响应时才炸,排查时容易被「请求成功了」迷惑。

根因:proxy 是 dev server 的运行时功能​

vite.config.ts 里的这段配置:

// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': 'http://127.0.0.1:3000',
},
},
})

它生效的场合只有一个:npm run dev 启动的开发服务器。Vite dev server 是一个 Node 进程,server.proxy 是这个进程里的转发中间件——浏览器请求 localhost:5173/api/agents,dev server 收到后转手发给 127.0.0.1:3000,把响应带回来。

npm run build 之后,产物是 dist/ 下的一堆静态文件,浏览器直接从 Nginx 拿文件——这个链路里没有 Vite 进程,server.proxy 配置留在 vite.config.ts 里,根本不随构建走。

所以生产的请求走向是:浏览器 → Nginx → ?。Nginx 按 location 匹配,没有 /api 规则时,静态文件找不到 /api/agents 这个路径,要么 404,要么被 SPA fallback 兜到 index.html——两种表现就是这么来的。

解法:Nginx 承担生产代理​

第一步:server 块加反代 location。

server {
listen 80;
server_name example.com;
root /var/www/app/dist;

# API 反代:前缀匹配,优先于 SPA fallback
location /api {
proxy_pass http://127.0.0.1:3000;
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;
}

# SPA fallback:其余路径全给前端入口
location / {
try_files $uri $uri/ /index.html;
}
}

要点两个:

  • proxy_pass 指向后端真实地址端口(示例的 3000 换成你的后端端口);/api 前缀请求会原样转发,后端路由也要带 /api 前缀
  • Nginx 前缀 location 按最长匹配优先,/api 天然赢过 /,两块不用调顺序;但如果你的 API 路径有更深的公共前缀,保证前缀匹配覆盖即可

第二步:验证后生效。

nginx -t          # 语法校验
nginx -s reload # 平滑重载

# 冒烟验证:应返回后端数据而非 HTML
curl -i https://example.com/api/health

curl 看到 JSON 响应、Content-Type: application/json,代理就通了;看到 text/html 说明请求还在被 fallback 接住。

顺带的收益:反代后前端和后端同域,开发时靠 Vite proxy 绕开的跨域问题在生产也自然消失,不需要再配 CORS。

这一篇与 部署后前端没更新?Nginx 缓存与构建产物检查 是部署排查的姊妹篇:那篇管「上线的代码是旧的」,这篇管「接口在生产没有代理层」,两处都查完,部署类 404 基本穷尽。

注意事项

location /api 的路径拼写必须和后端路由前缀完全一致。如果后端路由是 /api/agents 而 Nginx 反代配的是 /apis,或者 proxy_pass 末尾多了一个 /(会触发路径重写,/api/agents 变成 /agents 到达后端),都会得到新的 404。改完先用 curl -i 打一条真实接口确认响应头,再放流量。

常见问题​

生产环境 Nginx 反向代理 /api 怎么配置?​

在 server 块里加一个前缀 location:location /api { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; },然后 nginx -t 校验、nginx -s reload 生效。反代块的 /api 必须与后端路由前缀一致,且优先级高于 SPA 的 fallback location。

Vite 的 proxy 配置怎么确认生效了?​

dev server 启动日志不会打印 proxy 规则,直接验证请求走向:浏览器 Network 面板里请求 URL 仍是 http://localhost:5173/api/xxx 但响应数据来自后端,或用 curl http://localhost:5173/api/xxx 能拿到后端返回,就是生效了。proxy 只在 dev server(默认 5173 端口)上工作。

build 后 Vite 的 proxy 配置会打包进产物吗?​

不会。server.proxy 是 dev server 的运行时中间件,npm run build 产出的是纯静态文件,没有任何代理能力。生产环境的「代理」需要 Nginx 这类反向代理服务器承担,配置写在 Nginx 的 location 里而不是 vite.config.ts。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

GitHub Actions 部署失败却上线成功?pm2 竞争与 lockfile 漂移

· 阅读需 7 分钟

在 push 代码触发自动部署时,GitHub Actions 的 Deploy 步骤标红退出,登服务器一看服务却已经是最新版本;另一天则反过来——本地一切正常,CI 的 pnpm install --frozen-lockfile 每次必挂。这两个方向相反的信号失真,都出在部署链路本身,而不是代码。

在为客户开发电商自动化数据采集工具时遇到此类问题——批量抓取商品图片、SKU、价格与评价,清洗后导出结构化数据,支撑库存管理与竞品分析。该工具的服务端走 PM2 + GitHub Actions 部署,下面两类假故障都在这条链路上踩过。

TL;DR​

  • CI 红但部署实际生效:两条部署通道并发执行 pm2 restart,一方在重启瞬间查不到进程而误报失败。识别特征是 pm2.log 同一秒内既有成功重启记录、又有 process already online 报错。修法:部署收敛到唯一通道。
  • 本地绿但 CI 必挂:pnpm add 在子目录执行,只改了子包 package.json,根 pnpm-lock.yaml 没同步入库。修法:回到工作区根执行 pnpm install 重新生成 lockfile 并提交。

场景一:本地全绿,CI 必挂——pnpm lockfile 漂移​

这类失败与代码质量无关,纯粹是 pnpm-lock.yaml 与 package.json 失去同步——而 CI 是唯一会严格核对两者的环境。

现象​

CI 的 pnpm install --frozen-lockfile 每次都失败,报错固定:

ERR_PNPM_OUTDATED_LOCKFILE

而本地执行 install、build、测试全部通过。这种「本地构建正常但 CI 挂了」的组合很容易让人先怀疑 CI 缓存或 Node 版本,实际都无关。

根因:pnpm add 跑在了错误的目录​

这个项目是 pnpm workspace monorepo,lockfile 只有一份、位于工作区根。当时为了装一个依赖,执行了:

# 在 client/ 子目录里执行
pnpm --filter @ccl-ext/client add <pkg>

结果 client/package.json 更新了,但根目录的 pnpm-lock.yaml 没有同步重新生成——也就是没有入库。提交后,CI 拿到的 lockfile 与 package.json 不一致,--frozen-lockfile 校验直接拒绝安装。

本地测不出来的原因很直接:node_modules 已经物理装好了包,本地 install 直接复用现有产物,不会走到 frozen 校验;CI 是全新环境,每一次都严格比对 lockfile。

还有一个伴随症状可以当证据用:在错误的 cwd 下执行 pnpm 命令,还会在子目录生成一个游离的 client/pnpm-lock.yaml。看到这个文件出现,基本就是又跑错目录了。

修法​

两步:

  1. 回到工作区根,执行 pnpm install 重新生成根 lockfile,随代码一起提交;
  2. 今后 pnpm add / pnpm remove 一律在工作区根执行,不在子目录里跑。

CI 日志里失败原因的 specifiers diff(依赖声明差异对比)会明确指出哪个包的依赖范围变了,可以用来确认修的就是这一处。

多包工作区里还有一类相似的「归因错位」问题,见这篇:npm audit 报警归因错目录?多包部署先按 audited N 对包树。

场景二:GitHub Actions 报失败,部署实际成功——pm2 restart 并发竞争​

部署本身没有失败,失败的是并发撞车的第二次 restart——PM2 把这场竞争误报成了部署错误。

现象​

push 后 GitHub Actions 的「Deploy Server」失败在 pm2 restart 步骤:

[PM2][ERROR] Process 3 not found → exit 1

但登服务器核对:代码是最新、pm2 list 显示进程 online、健康检查返回 200。部署三要素全部正常,只有 Actions 认为自己失败了。

根因:两条部署通道同时 restart 同一个进程​

当时部署有两条触发路径:

  1. deploy.yml 的 push 自动触发;
  2. 本地的 /deploy 部署脚本(deploy.sh)。

同一次 push 会让两条路径各执行一次 pm2 restart ccl-ext-api。两个 restart 并发撞车时,一方恰好在另一方重启的瞬间去查询进程,查不到就报 Process not found 并以 exit 1 退出——PM2 把这次竞争当成了失败。

验证过程:pm2.log 里的同一秒​

判断是不是竞争,pm2.log 是最硬的证据。翻开日志,同一秒内能看到两类记录并存:

# 一边:重启完整走完,进程上线
Stopping → starting → online

# 另一边:撞车的那个 restart 查不到进程
PM2 error: process already online

一次重启「正在进行中」与另一次「查询进程」交错在同 1 秒内,就是竞争特征。加上代码最新、进程 online、健康检查 200 三项核对,可以确定部署实际生效,Actions 的失败是误报。

修法:移除 push 触发,部署收敛到唯一通道​

改法是删掉 deploy.yml 的 push 自动触发,只保留 workflow_dispatch 手动触发作为兜底:

on:
workflow_dispatch:

两个方案里选收敛通道而不是加并发锁,因为加锁只是让两条通道排队,部署路径依然有两条;而部署本来就应该只有一个入口,谁在什么时间部署了什么,只从一处发生。收敛后这类误报再没出现过。

部署后如何确认线上真的更新了(而不是构建缓存作祟),这篇有另一组排查思路:排查前端部署后线上未更新的问题。

注意事项

  • 保留的 workflow_dispatch 手动触发同样会与本地部署竞争,只在确认当前没有本地部署进行时使用。
  • 竞争窗口极短(重启的 1 秒内),但只要两条通道并存,push 频率越高撞上的概率越大,不是「偶发」而是「必现只是难复现」。
  • 判断假失败的顺序:先核对服务器三要素(代码、进程、健康检查),再看 pm2.log 有无同秒双记录,最后才考虑重跑。

根因对照:两类假故障的共同点是信号源被污染​

把两个场景放在一起看,共同点立刻浮现:出问题的都不是部署结果,而是产生信号的链路本身。

场景一场景二
表面信号CI 必挂,本地全绿Actions 失败,服务器已更新
真实状态lockfile 确实不同步部署已完成
污染源错误 cwd 的 pnpm 操作并发的第二条部署通道
硬证据specifiers diff + 游离子 lockfilepm2.log 同秒双记录

CI 的红与绿只是部署链路的输出,链路本身被污染(两份 lockfile、两条通道)时,信号就不再可信。先修链路,再看信号。

常见问题​

pm2 restart 会加载最新代码吗?​

会。pm2 restart 以磁盘上当前代码重启进程,所以竞争误报时服务器代码已经是最新。判断真假失败不看重启输出,而看 pm2.log 与健康检查:同一秒内同时出现成功重启记录和 process already online 报错,即竞争误报。

GitHub Actions 显示部署失败,怎么判断是不是误报?​

核对 3 项:服务器代码是否最新、pm2 list 进程是否 online、健康检查是否返回 200。三项都正常而失败点恰在 pm2 restart 步骤,基本可断定是并发竞争误报;把部署收敛到唯一通道后误报即消失。

pnpm install 本地成功,CI 报 ERR_PNPM_OUTDATED_LOCKFILE 怎么办?​

根因是 pnpm-lock.yaml 与 package.json 不同步:在子目录执行 pnpm add 只改了子包声明,根 lockfile 没有更新入库。回到工作区根执行 1 次 pnpm install 重新生成 lockfile 并提交即可;CI 日志里的 specifiers diff 会指出哪个包的依赖变了。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询