1. 先搞清楚要解决什么问题:前后端分离项目的部署本质
前后端分离这个词,估计做 Web 开发的人耳朵都快听出茧子了。Vue 或 React 这类前端框架负责页面渲染,Node 后端只提供 JSON 数据接口,两边独立开发、独立部署,听起来很美。可真到了上线的这一天,很多朋友会踩到一个非常经典的坑:浏览器打开页面一切正常,一调用后端接口就报 CORS 跨域错误,然后开始在代码里各种尝试加响应头、改请求头,折腾一宿也不一定能消停。
这里头的根源在于,本地开发时前端跑在 5173 或者 8080 这种端口,后端跑在 3000 端口,浏览器一看:哦,两个不同的源,按规矩直接拦截掉跨域请求。本地可以用代理工具绕过,但上了正式环境,前端静态文件交给了 Nginx,后端接口还是独立跑在服务器某个端口上,如果直接用 IP 加端口的方式去请求后端,跨域问题就会原封不动地出现在生产环境。
这篇文章要解决的,就是用宝塔面板 + Nginx + Node 这套组合,把前后端分离项目真正部署到一个“同源”的架构下面,让跨域问题从根上消失。适合谁看?自己买服务器折腾部署的开发者,或者想把项目从本地搬到线上、又不想靠后端开 CORS 硬扛的团队,都非常值得参考。整个方案不依赖复杂框架,就靠 Nginx 反向代理这一个成熟的常规操作,原理清楚了以后,不管前端是 Vue、React、还是 Angular,后端是 Koa、Express、还是 Egg,都能套用。
先给你透个底:这套部署做完之后,前端页面和后端接口会暴露在同一个域名、同一个端口下面,浏览器里完全符合同源策略,跨域问题自然就不存在了。Nginx 在其中扮演的角色很简单——它站在最外层接收所有外部请求,能处理的静态资源直接返回,遇到 API 请求就转发给内网里的 Node 服务,再把 Node 的响应原样传回浏览器。
下面我把整个部署流程拆开讲,每个关键步骤都说说为什么这么做。
1.1 前端、后端、Nginx 在生产环境里分别扮演什么角色
在开始动手之前,脑子里一定要有一张清晰的架构图。别急着打开面板点安装,先把三个角色的职责想明白,后面配置起来就有底气了。
前端项目,在开发模式下发出来的是一堆实时编译的文件,但生产环境里跑的是构建产物。拿 Vue 项目举例,执行npm run build之后会生成一个 dist 目录,里面的 index.html、CSS、JS 都是压缩过的静态文件。这些文件本身不需要什么特殊运行时,只要能通过 HTTP 服务被浏览器访问到就行。所以前端部分最简单的部署方式,就是把 dist 目录扔给 Nginx,让 Nginx 去充当静态文件服务器。
后端项目,也就是你的 Node 服务,它监听一个 HTTP 端口,比如 3000。它只负责业务逻辑、数据库读这些返回 JSON 或执行操作,不关心页面长什么样。生产环境中这个端口不该直接暴露给外部,一是没什么必要,二是直接暴露会增加被扫描和攻击的概率。更好的做法是让它在服务器内部监听,或者只监听内网地址,由 Nginx 统一对外提供入口。
Nginx 呢,就是最外面那个“看门大爷”。它在 80 或 443 端口接收用户的请求,然后根据请求路径做判断:如果请求的是静态文件资源,比如 /assets/xxx.js 或 /index.html,那就直接从磁盘上把文件读出来返回;如果请求路径以 /api 开头,说明这是要拿数据,就反向代理到内网里的 Node 服务,让 Node 处理完后把响应原路送回去。整个过程对浏览器来说,请求都是发给了同一个域名、同一个端口,完全看不出背后还藏着一个 Node 服务。
这套方案的妙处在于:浏览器永远不会直接跟 Node 打交道,它只认识 Nginx,所以所谓的“跨域”从一开始就被打消了。当然,浏览器自身的安全机制并没有被破坏,它仍然严格遵循同源策略,只不过我们通过 Nginx 让前后端物理上变成了“同源”。
1.2 为什么“后端开 CORS”感觉能用,生产环境却越来越别扭
说到跨域,很多人的第一反应是在后端代码里加 CORS 中间件,比如在 Express 里用 cros 插件配置 Access-Control-Allow-Origin 响应头。这种方案行不行?确实行,本地开发联调阶段,用它快速打通前后端很方便。但上线之后你会慢慢发现,它只是能跑,却一点都不优雅。
首先,CORS 的本质是“服务端允许浏览器跨域读取数据”,这相当于每发一次跨域请求,浏览器都要先发一个 OPTIONS 预检测请求来试探服务器允许不允许,服务器同意以后才发真正的请求。这样一来,一个简单的接口请求多了一次网络往返,响应速度在弱网环境下会肉眼可见地变慢。其次,在配置 Access-Control-Allow-Origin 时,如果给它写了 *,等于允许任意网站跨域读取你的接口数据,安全风险不言而喻。如果改成指定域名,又会出现多环境维护成本:测试环境一个域名、生产环境一个域名,配置要跟着环境走,稍不留神就漏改。
更重要的是,CORS 只解决了“浏览器不让读”的问题,但外部的人如果绕开浏览器,直接用 curl 或者 Postman 请求你的接口端口,照样能通,你相当于把很多本来可以藏在 Nginx 后面的接口细节直接暴露在了公网。而用 Nginx 做反代后,Nginx 还能顺手帮你做 HTTPS 卸载、日志记录、频率限制、静态资源缓存,这些是后端 CORS 想管都管不上的。
一句话总结:CORS 适合开发阶段快速调试,Nginx 反向代理才是生产环境里横跨前后端分离落地最稳妥的方案。接下来我就带你把这个方案从头到尾搭起来。
2. 部署前的准备工作:宝塔面板 + Nginx + Node 环境实战
在开始动手之前,先把服务器准备好。我这里假设你已经有一台云服务器,并且已经装好了操作系统。接下来整个服务器上的软件安装、目录管理、配置文件修改,我都会在宝塔面板里操作。宝塔的图形界面确实能省掉大量的命令行操作,尤其是文件管理、数据库、定时任务这些,对不习惯整天敲 Linux 命令的开发者很友好。
不过面板是“工具”,不是“捷径”。你仍然需要知道每个按钮背后在做什么,不然出了问题会完全不知道从哪里下手。所以我下面的每一步都会把要害讲清楚。
2.1 从零到一:宝塔面板安装与 Nginx 组件准备
如果你还没有安装宝塔,操作其实非常简单。登录服务器的终端,根据你当前使用的 Linux 发行版,在宝塔官网复制对应的一键安装命令执行即可。安装过程会拉取大量系统依赖,一般需要三五分钟,耐心等它跑完。
安装完修一个比较重要的事情:一定要第一时间修改面板的默认端口和默认登录入口路径,把密码换成足够复杂的强密码。这一步很多新手会跳过去,但别省。面板本身是一个 Web 服务,默认端口 8888 是公开资产,扫描器每天都在全网扫这种默认入口,不改等于把服务器管理权限挂在大街上。顺便可以去安全面板里开启系统防火墙,只放行必要的端口,比如 80(HTTP)、443(HTTPS)、SSH 端口,其它不常用的端口原则上一律拒绝外部访问。
首次进入面板,软件商店页面会提示你安装 Nginx、MySQL、PHP 等环境。如果你只是想部署 Node 项目,不需要动态网站和 PHP-FPM,那么就只装 Nginx 和 MySQL(如果后端要用到数据库的话)就够了,PHP 完全可以不装,减少不必要的服务就是减少一部分被攻击的可能。
这里说明一下:宝塔里的 Nginx 也不是什么魔改版本,它的本质就是官方 Nginx,通过编译或包管理方式预装好,再给套了一个可视化的配置托管。你手动修改 Nginx 配置时,面板会负责重新生成 main 配置和站点配置并重载服务,所以完全不用担心自己只能点“重启”而不能改底层。
等 Nginx 安装完成、服务状态显示“运行中”之后,可以通过http://服务器IP访问一下。正常情况下应该能看到 Nginx 默认页面,说明 HTTP 服务已经正常工作。
2.2 Node 运行环境与进程管理:装得上、活得久才算数
接下来是本项目的核心运行时 Node。在宝塔里安装 Node 有两条路,我都实际操作过,分别评价一下。
第一条,直接用宝塔软件商店里的“Node 版本管理器”或“PM2 管理器”插件。面板上选版本号、点一下安装,它就会自动帮你下载二进制包并配置好 PATH。优点是图形界面操作,省心,适合不太想折腾 Linux 环境变量的朋友。缺点是版本管理逻辑有时候比较隐晦,如果你想精细控制 Node 的全局模块安装位置,还得去翻阅它实际生成的环境变量脚本。
第二条,用 NVM(Node Version Manager)自己装。这算是比较标准的 Node 多版本管理方式。虽然面板提供了可视化安装,但对于以后可能要在服务器上频繁切换 Node 版本的开发者,NVM 的优先级更高一些。
安装完成后先验证一下,命令行输入node -v和npm -v,都输出了版本号才能确认环境没问题。这里有个很容易踩的坑:很多人在终端里安装了 NVM,但用宝塔自带的“终端”工具登录去执行 node 命令却提示 not found。原因多半是 NVM 的环境变量写在.bashrc或.profile里,而这个新增的 Shell 会话没有重新加载配置。解决办法是执行一下source ~/.bashrc,或者退出终端重新进一次。
Node 装好了,项目还得有一个稳定的守护进程,否则 Node 服务一崩就没人把它拉起来,网站直接陷入后台“挂掉”的状态。这里我最推荐的就是 PM2,这也是宝塔面板里单独提供管理界面的进程管理工具。PM2 做三件很核心的事:第一,当进程异常退出时自动重启;第二,开机自启动,服务器重启后 Node 服务跟着起来;第三,集中记录 stdout 和 stderr 日志,排查问题很方便。
安装 PM2 用npm install -g pm2一行命令搞定。启动项目也是pm2 start app.js这样简单的命令,后面详情会在部署小节具体演示。
2.3 前端打包与后端准备:目录规划直接影响后续排错
在往服务器上传代码之前,先把服务器上的目录结构规划清楚。烂项目的状态是文件散落各处,遇到问题无从查起。我这里分享一个自己长期在用的规范目录方案,不一定是最完美,但胜在直观:
/www/wwwroot/myapp/ # 整个应用的总目录 ├── frontend/ # 前端项目 │ ├── dist/ # 构建产物,Nginx 的静态根目录 │ └── source/ # 前端源码,可选保留 ├── backend/ # Node 后端项目 │ ├── app.js # 后端入口文件 │ ├── package.json │ └── logs/ # 应用日志目录前端在本地执行npm run build,得到 dist 目录之后,用宝塔文件管理器上传到服务器的 /www/wwwroot/myapp/frontend 下,再把文件夹重命名为 dist。如果你使用 Git 工作流,宝塔自带的可视化 Git 工具可以省去手动上传的麻烦,直接在服务器上拉取指定分支,然后自己执行构建命令也行。后端就更简单了,把整个项目拷贝或拉取到 backend 目录,进入目录执行npm install --production安装生产依赖,然后就可以启动。
后端代码里有个细节值得注意:不要把数据库密码、密钥这类敏感配置直接硬编码在源码里,用环境变量去注入。宝塔面板的“环境变量”我不会依赖,我更习惯用一个.env文件,启动时加载它。很多 Node 框架天然支持读取 env 文件,如果你用的框架不支持,可以用 dotenv 这个小工具。部署时这份 env 文件只放在服务器上,不进 Git 仓库,最大程度避免密钥泄露。
3. Nginx 反向代理与同源化配置:跨域的终结区
等环境都就位了,下面进入本文的绝对核心环节:Nginx 的配置。这一步做好了,跨域问题彻底解决;做不好,前面所有功夫都是白搭。我会先讲清楚反向代理为什么能消除跨域,然后直接给出一份能用的完整配置,再逐段拆解每一行的作用和容易改错的细节。
Nginx 消除跨域的深层逻辑:统一“源”的概念
浏览器里的跨域判断,基于的是“源”,也就是协议 + 域名 + 端口这三个东西的组合。普通浏览器请求的页面是http://www.example.com:80,如果这个页面里的 JavaScript 发起请求到http://localhost:3000,源就不一致,浏览器下达拦截指令;但是如果页面的请求目标是http://www.example.com:80/api/xxx,协议、域名、端口全部一致,那么这个请求就是同源的,浏览器放行。
Nginx 反向代理干的事,就是在用户不感知的情况下,把同一个源下的/api/*请求转发到另一个内网地址。虽然背后真正处理请求的是 3000 端口的 Node 服务,但浏览器永远只看到 Nginx 的域名和端口。因为浏览器根本不认识也没有直接接触到 Node 服务,所以 CORS 的那套限制根本不会触发。
这就是为什么我一直强调“不要在 Nginx 配置里去找一个叫跨域开关的东西”。Nginx 没有开关,它只需要做反向代理,跨域就自然被“架构”掉了。如果你在前端代码里写了绝对的http://localhost:3000这种地址,那后端怎么代理都没用,因为浏览器看到的 API 地址仍然是另一个源。正确的方式是前端只写相对路径/api/xxx,让请求被自动拼上当前页面的域名。这也是为什么前后端分离项目里,接口地址建议统一加一个固定的前缀(比如 /api)来做区分和转发。
3.1 一份可以直接落地的 Nginx 站点配置
在宝塔面板里,新建一个站点,填上你的域名,PHP 版本选择“纯静态”。这不会创建任何 PHP 配置文件,只帮你把站点的目录和 Nginx 站点配置文件生成好。下一步,点击站点设置 → 配置文件,就能看到一份 server 块,默认只是做了静态文件服务。我把改造后的完整配置直接贴出来,你在此基础上微调域名和目录就行。
server { listen 80; server_name your-domain.com; # 前端静态资源根目录 root /www/wwwroot/myapp/frontend/dist; index index.html; # 前端 SPA 路由:刷新页面时不 404 location / { try_files $uri $uri/ /index.html; } # 后端 API 反向代理 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; proxy_connect_timeout 60s; proxy_read_timeout 60s; proxy_send_timeout 60s; } # 针对可能用到的 WebSocket 连接单独配置 location /ws/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } }这份配置有一个很关键的点:在location /api/块里,proxy_pass的地址末尾加了一条斜杠,http://127.0.0.1:3000/。这个斜杠不是随便加的,它决定路径传递方式。当你请求http://your-domain.com/api/login时,因为 proxy_pass 里有斜杠,匹配/api/之后的路径/login会拼在 3000 后面,实际转发给 Node 的地址是http://127.0.0.1:3000/login。也就是说,/api前缀在转发时被“剥掉”了,后端路由不用加/api。
另一种写法是 proxy_pass 不带斜杠:
location /api/ { proxy_pass http://127.0.0.1:3000; }这样请求/api/login会原封不动地转发成http://127.0.0.1:3000/api/login,后端就必须在路由层包含/api前缀。两种写法的选择完全取决于你后端代码的路由定义方式。如果后端的接口全部都统一挂在/api下,那不带斜杠写起来更简单;如果后端内部就是/login、/user这种干净路由,那就用带斜杠的写法。在实际操作中,新手最容易在这个斜杠上吃亏,明明配了代理但后端总是报路由 404,多半就是这里没配对。
3.2 静态文件与 SPA 路由的处理细节
静态网站文件处理那一块,location /我用了一行try_files $uri $uri/ /index.html;。这行的意思很简单:请求进来时先试着找对应文件,找不到就去找对应目录,目录也没有就兜底返回 index.html。这正好适合 Vue、React 这种使用了 HTML5 History 模式的路由——你在页面内部点击跳转没问题,但如果直接刷新/about这个地址,Nginx 会拿/about去磁盘找文件,结果当然找不到。有了 try_files,它就会把这个请求降级成返回 index.html,然后前端框架自行路由。
如果你的前端使用 hash 模式,即地址栏带#/about,那么刷新不存在这个 404 问题,但整体体验不如 history 模式干净,所以我默认按 history 模式来写。
至于 WebSocket 的配置,是我后期踩过坑之后补上的。很多 Node 后端会用到 WebSocket 做实时消息推送,比如通知、聊天或协同编辑。WebSocket 协议升级依赖Upgrade和Connection这两个 header,Nginx 默认不会自动转发,必须显式声明。我见过不少项目,HTTP 接口都跑得好好的,WebSocket 连接却一直断,要么挂在握手阶段,要么刚连上就断开,很大原因就是少了这一段配置。如果你的后端没有 WebSocket 需求,这一段可以直接删掉,不影响常规 API。
3.3 配置完成后的验证与重载流程
写完配置先不要急着点赞,做三件事:验证语法、重载服务、模拟请求。
宝塔面板上可以直接执行nginx -t来检查配置语法。这一步很实用,它可以提前发现哪些地方少了分号、引号没闭合、目录不存在。如果提示 OK,再执行nginx -s reload,这个命令是“平滑重载”,不会中断现有连接,属于无痛生效。直接重启 Nginx 也不是不行,但在生产环境里能平滑就别粗暴重启,毕竟一次优先级高的反向代理重启通常只有几十毫秒,但总有那么点概率造成正在进行的请求失败。
模拟请求这一步推荐在服务器命令行里用 curl 完成,尽量不依赖浏览器。先测静态资源:
curl -I http://your-domain.com/如果返回 200 和 text/html,说明前端页面正常。再测一下后端接口,绕开代理直接请求 Node 服务:
curl -I http://127.0.0.1:3000/health确认内网访问正常后,再走完整链路:
curl -I http://your-domain.com/api/health如果这最后一条也返回 200,那说明 Nginx 成功把请求转给了 Node。浏览器里打开页面,接口调用应该就是干净利落的一次同源请求,想让它报跨域都难。
4. 前后端代码侧如何配合:开发环境到生产环境的无缝衔接
Nginx 只是解决了生产环境的问题,但如果你在前端代码里写死了本地调试地址,部署之后又要大改一遍,那就有点难受了。比较理想的状态是:开发环境和生产环境请求同一个接口前缀,换的是环境配置,而不是代码逻辑。
4.1 开发环境用 Vite 或 Webpack 代理:让本地请求也走同源路线
现在的 Vue 或 React 项目,开发服务器绝大多数都是 Vite 或者 Webpack Dev Server。它们本身自带一个代理特性,可以把请求转发到任意后端地址。我以 Vite 为例,在 vite.config.js 里这样配置:
export default { server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, }, };这样前端代码里所有请求都写成/api/login、/api/user/info这类相对路径,开发环境下会被 Vite Dev Server 代理到 localhost:3000,生产环境下 Nginx 会直接把/api转发到本机 3000 端口。前后端代码不需要为环境做任何区分,只改部署工具和配置即可。
有人可能会问,开发环境直接配置跨域不就行了,为什么还要用代理?因为代理模式下的网络请求是“同源的”,你在调试面板里看到的地址就是http://localhost:5173/api/login,跟生产环境最终形态几乎一致,能够提前发现一些因为路径前缀写错导致的问题。另外,配置代理以后,浏览器不会发出 OPTIONS 预检请求,减少了一部分无谓的网络开销,调试起来也更干净。
4.2 后端监听地址选择:安全与可访问性之间的平衡
后端部分,可做的配合也不少。第一件事是把监听地址收紧。举个例子,很多 Node 框架启动时会默认监听 0.0.0.0,也就是对所有网络接口开放。如果服务器有公网 IP,外部请求也能直接访问这个端口,这样即使做了 Nginx 反代,后端的 3000 端口实际上也是裸露的。更稳的方式是监听 127.0.0.1,只有本机能访问:
app.listen(3000, '127.0.0.1', () => { console.log('API server listening on 127.0.0.1:3000'); });这么做的好处很明显:第一,公网无法直接扫到你的后端端口,安全风险小很多;第二,端口只对本机开放,不用在云服务器的安全组里额外额外放行。第三,Nginx 和 Node 在同一台机器上,走回环地址访问性能上也是最优的。
当然也有例外:如果你的 Node 服务和 Nginx 不在一台服务器上,比如后面打算做横向扩展,那就不能让 Nginx 去代理 127.0.0.1 了,得改成内网 IP。这是后面扩展负载时要考虑的事,现在单机部署阶段先绑定 127.0.0.1,简单可靠。
4.3 后端保留兜底 CORS:什么时候需要,什么时候可以完全不用
我前文说了生产环境不加 CORS 也能正常运行,但“能不用”不等于“绝对不能留”。我这里给出的建议是:后端保留一个宽松度很低、白名单明确的 CORS 配置,用来应对特殊情况。
比如你的项目里有一个图片生成服务,部署在另一个域名,这个接口需要从别的站点直接调用;又比如你给某个非 Web 客户端或小程序提供了 API,客户端未必都走同源策略。这种情况下 CORS 就不能完全去掉,但一定要配置成白名单模式,而不是Access-Control-Allow-Origin: *。具体代码里可以读取环境变量中的允许域名列表,把线上域名放进去。
如果确定所有流量都走 Nginx,后端完全可以不处理 CORS,代码也少一些,日志也会干净。即便是保留兜底配置,也不要在 Nginx 的 location /api 块里去额外添加 CORS 头,这会让多个环节都在处理跨域,出了问题反而很难排查。保持第 3 节那份配置的纯粹性,把跨域问题的处理收敛在同一层,是我在实际运维中最推荐的策略。
5. 部署中遇到的常见坑与排查记录
网上很多教程都是顺风顺水讲完配置就收尾,但真实部署的时候,谁不是踩过几个坑才把服务跑起来的?这一节我把自己实际遇到过的、身边朋友也问过的问题整理成了速查表,并按一个标准排查流程走一遍,你在现场如果遇到类似问题,直接照着做就行。
5.1 常见错误一则速查表
| 典型表现 | 直接原因 | 处理方法 |
|---|---|---|
| 页面白屏,Network 面板里 JS/CSS 返回 404 | 静态资源目录路径写错,dist 没上传到 root 指定的位置 | 检查 Nginx 的 root 路径与 dist 实际位置是否一致 |
| 接口报 502 Bad Gateway | Node 服务没有监听 Nginx 转发的那个端口 | 先 curl 127.0.0.1:3000 验证后端是否可用,再检查 proxy_pass 端口 |
| 接口报 504 Gateway Timeout | Node 服务卡住或进程已崩溃但没退出 | 查看 PM2 状态,看日志是否有未捕获的异常 |
| 刷新前端路由页面报 404 | SPA 路由 history 模式失效 | 补上 try_files $uri $uri/ /index.html; |
| 页面能打开,但接口请求 URL 是 http://localhost:3000 | 前端代码里写死了后端完整地址 | 改成相对路径 /api/xxx,走 Nginx 反向代理 |
| 前端能访问,接口 OPTIONS 请求一直 403 | 后端未正确响应预检请求,或者 Nginx 拦截了请求体过大的请求 | 查看 Nginx 错误日志,检查 client_max_body_size 配置 |
| WebSocket 连接一直握手失败 | Nginx 未转发 Upgrade 头 | 补上 proxy_set_header Upgrade 和 Connection |
5.2 标准排查流程:从外到内一层层剥洋葱
遇到部署问题,最忌讳一上来就改配置然后碰运气。我的排查顺序一般是固定的:浏览器看现象 → 服务器上 curl 完整链路 → curl 后端端口 → 看日志。
第一步,在浏览器 F12 控制台看具体的报错和请求 URL。这一步能区分很多情况:如果请求根本没发出去就报网络错误,大概率是前端地址写错或者 HTTPS 混用问题;如果请求发出了但返回 5xx,那问题就在服务端。
第二步,在服务器上执行curl -v http://your-domain.com/api/health看完整链路。curl 会打印 HTTP 状态码、响应头、每个分阶段消耗的时间。如果是 502 或 504,说明 Nginx 的连接尝试出了岔子。然后执行curl -v http://127.0.0.1:3000/health,如果内网直接访问都失败,说明 Node 服务根本没起来;如果内网能通但走 Nginx 就失败,那原因一定出在 Nginx 配置、端口绑定或者防火墙上。
第三步,看日志。Nginx 的站点访问日志默认在 /www/wwwlogs/your-domain.com.log,错误日志在同目录下。里面的线索通常写得非常直白,比如connect() failed (111: Connection refused),回给人你的直觉就是后端端口没开;比如upstream timed out,说明 Nginx 在等上游响应但超时了。Node 项目如果用了 PM2,可以pm2 logs看应用输出;如果是自己手写的日志文件,就去 backend/logs 下翻。绝大多数线上问题的答案,都在日志文件里老老实实等着你呢。
5.3 我踩过的三个细节坑,写出来帮你省半天时间
第一个坑,前端资源缓存的问题。部署新版本后,用户浏览器还在用旧的 index.html,而 index.html 引用的 JS 文件名是按照旧版本编译出来的,结果因为旧文件已被清理而 404。表面上像是部署出了问题,实际上是 Nginx 或浏览器层面的缓存策略没配合好。解决思路是给静态资源加上长时间的强缓存,但 index.html 要设置成不缓存,具体在 Nginx 里就是:
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 30d; add_header Cache-Control "public, no-transform"; }这样浏览器缓存住那些哈希后文件名唯一不变的静态资源文件,index.html 每次都回源拉取最新的引用,就既快又不会出现过期资源的问题了。
第二个坑,上传文件的请求体大小限制。默认情况下 Nginx 的client_max_body_size是 1m,也就是说只要上传超过 1MB 的文件就会直接返回 413 Request Entity Too Large,很多人排查半天最后发现是 Nginx 在门口把大文件拦了。如果你的项目涉及图片上传或文件导入,一定要在 server 块或者 location 块里加一行,比如client_max_body_size 50m;,监听范围覆盖到具体接口即可,别为了省事直接全局放开成超大值,太多无意义的请求体占用带宽资源也不是好事。
第三个坑,proxy_pass里边的斜杠问题。我前面已经讲过带斜杠和不带斜杠的区别,在这里再强调一次:如果你配了代理但后端日志里总是出现POST /api/login 404之类的记录,说明埋点那个/api前缀没有被剥掉,后端没有对应这个路由。处理方案有两种:要么改 proxy_pass 的写法,要么在后端路由里统一挂上/api前缀。无论选哪种,一定要让前端、Nginx、后端三处的路径规则保持一致,这三处是互相配合的“三角关系”,任何一边的误解都会导致整个链路断裂。
6. 上线之后我觉得真正值得做的事
走到这一步,一个没有跨域烦恼的生产环境就算真正搭建好了。不过部署完成不等于彻底结束,等到系统上线稳定运行之后,我建议你回头做几件事,虽然不紧急但性价比很高。
第一件事,把 HTTPS 补上。Nginx 反代天然适合在边缘层终止 SSL,证书申请、续期、加密转换这些都在 Nginx 这一层完成,后端 Node 全程跑的是内部 HTTP 明文请求,最少改动就能让整个站点拥有加密通道。宝塔面板自带免费的 SSL 证书申请和续期功能,操作起来十分钟之内就能搞定。弄完之后在配置文件里把 HTTP 的全部流量 301 跳转到 HTTPS,用户访问体验和安全系数都会上一个台阶。
第二件事,给涉及数据库或其他外部服务的操作加上连接失败重试机制。Node 单线程模型决定了某个异步操作卡死可能拖垮整个接口,而不是像多线程那样只是牺牲一个工作线程。上线一段时间后观察 PM2 的日志,你通常会发现了不少偶发的数据库连接池耗尽、第三方 API 超时。给它加上重试和熔断,比单纯加大超时时间要管用得多。
第三件事,可以考虑做一个简单的部署脚本。手动上传文件再执行构建的过程,次数多了总会出错,脚本能保证每次操作一致。我自己的做法是在服务器上放一个 deploy.sh,内容就是拉代码、装依赖、构建、用 PM2 重启。
#!/bin/bash cd /www/wwwroot/myapp/backend git pull origin main npm install --production pm2 restart app前端构建也可以写进同一个脚本里,但前端构建涉及的步骤更多,建议分开,保证不同节奏的发布不会互相干扰。熟练之后你会发现,整个部署流程变得极度可控:改完代码、推送、服务器上执行一条脚本,几十秒钟就完成了上线过程。
根据我多次部署前后端分离项目的经验,这套“宝塔 + Nginx + Node”的架构就像一个稳定的大后方:它不会给你带来额外的框架依赖,也能在你将来把单体拆成多个微服务时平滑过渡——无非是在 Nginx 里再加几个 location 块、多代理几个内部端口而已。把反向代理这个基本功练扎实了,以后再碰到前后端分离相关的部署问题,基本就都是爽局了。