Next.js 生产部署防坑手册:从一次 502 事故到三层排查法
适用场景:服务器部署 Next.js / Nuxt.js 等 Node.js 项目,使用 Nginx 反向代理 + PM2 进程管理。
核心原则:让每一步都可验证、可回溯、不依赖玄学。
一、架构全景:你到底在部署什么
在动手之前,先搞清请求链路上每一层的职责。这是所有排查工作的地图。
用户浏览器
│ HTTPS
▼
Nginx(80/443 端口)── 反向代理 ──▶ Node.js 服务(127.0.0.1:3000)
│
PM2(进程守护)
| 层级 | 职责 | 挂了的表现 |
|---|---|---|
| Nginx | 处理 HTTPS、静态资源、转发请求 | 浏览器显示 502 / 连接被拒绝 |
| PM2 | 保活 Node 进程、自动重启 | 端口无监听 |
| Node 服务 | 真正处理业务逻辑 | Nginx 报 Connection refused |
关键认知:三层各司其职,任何一层出问题都会表现为 502,但根因完全不同。定位问题的第一步就是判断”是谁挂了”,而不是盲目重启。
二、标准部署流程:照抄即可
2.1 环境准备
# 确认 Node 版本(建议 LTS)
node -v
npm -v
两个”不建议”:
- ⚠️ 不建议用 Corepack 管理 pnpm 来做生产启动
- ⚠️ 不建议全局安装 pnpm(容易和 Corepack 冲突)
2.2 项目构建
cd /var/www/example.com
pnpm install
pnpm run build
2.3 PM2 启动(核心步骤)
核心思想:绕过
pnpm,直接用node启动 Next.js 的二进制文件。这是本文最重要的一个决策。
cd /var/www/example.com
cat > ecosystem.config.js << 'EOF'
module.exports = {
apps: [{
name: "my_next_app",
script: "node",
args: "node_modules/next/dist/bin/next start -p 3000",
cwd: "/var/www/example.com",
interpreter: "node",
env: {
NODE_ENV: "production",
PORT: 3000
}
}]
}
EOF
pm2 start ecosystem.config.js
pm2 save
pm2 startup # 执行输出的 sudo 命令,设置开机自启
2.4 Nginx 反向代理
server {
listen 443 ssl http2;
server_name example.com;
# SSL 配置(宝塔自动生成即可)
location / {
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;
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_cache_bypass $http_upgrade;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
安全提示:生产环境 Next.js 应只监听回环地址(
127.0.0.1:3000),对外仅开放 Nginx 的 80/443 端口,避免 3000 端口从公网被直连扫描。
2.5 验证(必做)
ss -lntp | grep 3000 # 端口有人听吗?
curl http://127.0.0.1:3000 # 本地能通吗?
curl https://example.com # 外网能通吗?
三、血泪复盘:一次真实事故的全记录
事故时间线
| 步骤 | 操作 | 结果 |
|---|---|---|
| 1 | pnpm run build |
✅ 成功 |
| 2 | pm2 restart my_next_app |
⚠️ 进程假在线 |
| 3 | 浏览器访问 | ❌ Nginx 502 |
| 4 | 查看日志 | 发现 SyntaxError |
| 5 | 排查端口 | 3000 无监听 |
| 6 | 定位原因 | Corepack pnpm 不在 PATH |
| 7 | 根因 | 宝塔 start.sh 被 PM2 当 Node 脚本执行 |
根因一句话
PM2 用 Node.js 去执行了一个 Bash 脚本(
start.sh),Bash 的export语法在 Node 里是非法 Token,直接抛SyntaxError,进程秒退,3000 端口永远没人监听。
修复方案
删除原进程,用 node 直接启动 Next.js 二进制(即上文 2.3 节的配置),端口立即恢复监听,502 消失。
四、五大经典坑位深度解析
坑 1:PM2 显示 online 但端口没监听
| 现象 | 原因 | 解决 |
|---|---|---|
pm2 list 显示 online |
进程启动了但瞬间崩溃 | pm2 logs 项目名 看真实报错 |
ss -lntp | grep 3000 无输出 |
服务没真正起来 | 检查启动命令是否正确 |
口诀:online ≠ 正常运行,必须以 ss -lntp 为准。
坑 2:宝塔 + Corepack + pnpm = 玄学三件套
| 组合 | 问题 |
|---|---|
宝塔用 start.sh 启动 |
PM2 可能用 Node 去解析 Shell 脚本 → SyntaxError |
| Corepack 的 pnpm | 不在系统 PATH 里,宝塔环境拿不到 |
| 全局没装 pnpm | pnpm: command not found |
✅ 正确做法:生产环境直接用 node node_modules/next/dist/bin/next start,彻底绕过 pnpm。
坑 3:端口不一致
| 场景 | 表现 |
|---|---|
| Next.js 默认跑 3000,但被改成 3001 | Nginx 转发 3000 → Connection refused |
.env 里设了 PORT=8080 |
同上 |
排查命令:
ss -lntp
grep -R "PORT" .env* 2>/dev/null
坑 4:Nginx 报 Connection refused
含义:Nginx 能正常接收请求,但后端 127.0.0.1:3000 没服务。
排查顺序(记住这个顺序,不要跳步):
ss -lntp | grep 3000→ 端口有没有人听pm2 list→ 进程在不在pm2 logs 项目名→ 启动有没有报错curl http://127.0.0.1:3000→ 服务本身能不能响应
坑 5:pnpm start 在前台跑,关 SSH 就死
原因:直接在终端运行 pnpm start,进程依附于 SSH 会话。
解决:用 PM2 管理(推荐),或用 nohup pnpm start &。
五、运维 SOP:让每次操作都可重复
5.1 更新部署(每次发版)
cd /var/www/example.com
git pull
pnpm install
pnpm run build
pm2 restart my_next_app
sleep 3
ss -lntp | grep 3000
curl http://127.0.0.1:3000
5.2 紧急恢复
pm2 list
pm2 logs my_next_app --lines 50
# 进程没了 → 重启
pm2 start ecosystem.config.js
# 端口被占 → 杀进程
lsof -i :3000
kill -9 <PID>
# 极端情况:重装依赖
rm -rf node_modules .next
pnpm install
pnpm run build
pm2 start ecosystem.config.js
六、进阶配置模板
基础版(单端口)
module.exports = {
apps: [{
name: "my_app",
script: "node",
args: "node_modules/next/dist/bin/next start -p 3000",
cwd: "/var/www/example.com",
interpreter: "node",
env: { NODE_ENV: "production", PORT: 3000 }
}]
}
进阶版(多实例 + 内存限制 + 日志分离)
module.exports = {
apps: [{
name: "my_app",
script: "node",
args: "node_modules/next/dist/bin/next start -p 3000",
cwd: "/var/www/example.com",
interpreter: "node",
instances: "max", // 使用所有 CPU 核心
exec_mode: "cluster", // 集群模式
max_memory_restart: "512M", // 内存超限自动重启
env: { NODE_ENV: "production", PORT: 3000 },
error_file: "/var/log/my_app/error.log",
out_file: "/var/log/my_app/out.log",
merge_logs: true,
log_date_format: "YYYY-MM-DD HH:mm:ss"
}]
}
七、诊断命令速查卡
# ===== 端口监听 =====
ss -lntp | grep 3000
# ===== PM2 状态 =====
pm2 list && pm2 logs 项目名 && pm2 restart 项目名
# ===== 本地连通性 =====
curl http://127.0.0.1:3000 && curl https://你的域名.com
# ===== Nginx 状态 =====
nginx -t && systemctl status nginx && tail -f /var/log/nginx/error.log
# ===== 进程详情 =====
ps aux | grep node && lsof -i :3000
# ===== 开机自启 =====
pm2 startup && pm2 save
八、总结:三条核心要点与一句金句
三条核心要点
- 端口监听是唯一的真相源——PM2 显示
online只代表进程被创建过,不代表服务在跑。ss -lntp | grep 3000才是最终确认手段。 - 生产环境绕过包管理器,直接调用二进制——用
node node_modules/next/dist/bin/next start启动,彻底规避 Corepack、pnpm PATH、Shell 脚本解析等层层陷阱。 - 排查永远遵循”端口 → 进程 → 日志”的三层递进顺序——不要跳步,不要靠猜,不要一上来就重装依赖。
一句金句
当你看到
Connection refused,不要调 Nginx,不要重装依赖,不要百度 502——先看端口,再看进程,再看日志。三层下来,99% 的问题无处可藏。
文档版本:v1.0 | 整理日期:2026-08-19 | 案例来源:某生产环境真实排障记录