Next.js 生产部署防坑手册:从一次 502 事故到三层排查法

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 没服务。

排查顺序(记住这个顺序,不要跳步):

  1. ss -lntp | grep 3000 → 端口有没有人听
  2. pm2 list → 进程在不在
  3. pm2 logs 项目名 → 启动有没有报错
  4. 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

八、总结:三条核心要点与一句金句

三条核心要点

  1. 端口监听是唯一的真相源——PM2 显示 online 只代表进程被创建过,不代表服务在跑。ss -lntp | grep 3000 才是最终确认手段。
  2. 生产环境绕过包管理器,直接调用二进制——用 node node_modules/next/dist/bin/next start 启动,彻底规避 Corepack、pnpm PATH、Shell 脚本解析等层层陷阱。
  3. 排查永远遵循”端口 → 进程 → 日志”的三层递进顺序——不要跳步,不要靠猜,不要一上来就重装依赖。

一句金句

当你看到 Connection refused,不要调 Nginx,不要重装依赖,不要百度 502——先看端口,再看进程,再看日志。三层下来,99% 的问题无处可藏。


文档版本:v1.0 | 整理日期:2026-08-19 | 案例来源:某生产环境真实排障记录

上一篇
下一篇