背景
原先的项目采用纯前端部署模式:Vue打包后扔进Nginx静态目录,API由后端负责,前端通过Nginx代理转发。部署过程简单直接。后来为了给前端增加数据聚合能力,引入了Node BFF层,架构变为:浏览器 -> Nginx -> BFF(端口A) -> 后端服务。看似只加了一层,却引发了令人头疼的接口502问题。
最初的尝试:Nginx动静分离方案
计划让Nginx负责前端静态文件,同时将/api请求代理给BFF。配置文件大体如下:
nginx
location /api/ {
proxy_pass http://127.0.0.1:端口A;
}
BFF正常运行,本地curl验证通过,但前端访问接口始终返回502。此后陷入了漫长的排查。
踩过的坑
-
Express新版路由语法变更
旧代码中的
app.get('*')在新版依赖中不再合法,导致BFF进程启动失败。虽然PM2显示online,但端口并未监听。最终改用app.use()中间件解决。 -
IPv4与IPv6监听差异
Node默认可能优先监听IPv6地址(
::),而Nginx配置的proxy_pass使用的是IPv4地址127.0.0.1,两者协议不匹配导致连接拒绝。强制BFF监听0.0.0.0后解决。 -
PM2环境变量未加载
使用
pm2 restart时环境变量未被重新读取,导致端口回退到默认值。改用pm2 delete后重新启动并明确注入环境变量才最终生效。 -
Nginx配置文件路径混乱
主配置中定义的错误日志路径并不存在,实际生效的配置文件位于另一个子目录。反复修改错误文件自然无济于事。
根本原因:Docker网络隔离
在排查过程中忽略了一个关键:Nginx运行在Docker容器内,而BFF直接跑在宿主机上。容器内的127.0.0.1指向容器自身,并非宿主机。因此Nginx永远无法通过127.0.0.1连接到宿主机的BFF服务,这是502的根源。
最终方案:BFF全包模式
考虑到时间成本与维护复杂度,决定暂不深入改造Docker网络,而是回退到更简洁的BFF全包模式:
- BFF同时托管前端静态文件与API接口。
- 前端打包产物直接放入BFF的
public目录。 - BFF监听
0.0.0.0,对外暴露单一端口。 - 去除Nginx这一中间层,彻底规避容器网络隔离问题。
修改后的核心代码结构:
javascript
// 静态文件托管
app.use(express.static(path.join(__dirname, 'public')));
// API路由
app.use('/api', apiRouter);
// SPA fallback
app.use((req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});
部署后服务恢复正常,前端页面与接口均工作良好。
经验与反思
- 画清架构拓扑:在调试前先明确请求流转经过的每一层,特别是涉及容器、虚拟机时,务必弄清网络边界。
- 逐层验证连通性:从最内层开始用curl测试,确保每一跳可达。BFF本地可访问而Nginx不行,立刻应怀疑中间链路。
- 容器内localhost不等同于宿主机 :这是容器化部署中最容易忽略的知识点。要让容器访问宿主机服务,应使用
host.docker.internal或宿主机内网IP。 - 全包模式在小规模项目中的优势:当不需要复杂负载均衡或HTTPS时,让Node全包可大幅降低运维复杂度,心智负担最小。