1. 这不是本地调试是真刀真枪上生产环境你写完一个 Node.js 应用node app.js一跑localhost:3000 打开页面能动心里一松——完事了别急。这行命令在开发机上跑得再欢扔到 Ubuntu 22.04 的服务器上就是个裸奔的进程没守护、没日志轮转、没内存监控、没自动重启、没 HTTPS、没反向代理、没请求限流、没静态资源缓存……更关键的是它会直接暴露在公网端口上连个防火墙兜底都没有。我见过太多项目上线第一天就因为某个未捕获的 Promise Rejection 导致整个服务静默退出用户打不开页面运维查日志发现进程早没了而ps aux | grep node返回空——这种“消失式故障”在生产环境里是最要命的。Ubuntu 22.04 是 LTS 版本内核稳定、软件源成熟、安全更新周期长是绝大多数企业级 Node.js 服务首选的操作系统基底。但它的“稳定”不等于“开箱即用”。它默认不装 Node.js不配 Nginx不启防火墙不设非 root 用户权限隔离。这些恰恰是生产环境的铁律Node.js 进程绝不能用 root 启动Web 流量必须经由 Nginx 做第一道过滤所有外部访问必须走 80/443而非直接暴露 3000、3001 这类高危端口。网上那些“一行命令安装 Node.js”的教程90% 都在教你怎么把开发习惯带进生产环境。比如curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs这种方式装出来的 Node.js虽然版本新但二进制路径、全局模块位置、权限模型和生产部署所需的最小化、可审计、可回滚要求完全不匹配。PM2 和 Nginx 不是可选项是生产环境的“呼吸系统”。PM2 负责让 Node.js 进程活下来——它不是简单的进程守护而是集进程管理、内存监控、CPU 限制、日志聚合、集群模式cluster、零停机热重载reload于一体的运行时平台。Nginx 则是你的“数字门卫”它处理 SSL/TLS 终止、HTTP/2 支持、静态文件缓存、Gzip 压缩、请求头过滤、IP 白名单、DDoS 初步防护、负载均衡未来横向扩展时、以及最关键的——把/api/*的请求反向代理给后端 Node.js把/static/*的请求直接返回磁盘文件。这两者组合才构成一个现代 Web 服务的最小可行架构。你看到的warning: this is a development server. do not use it in a production deployment提示不是 Node.js 在谦虚是在严肃警告它的内置 HTTP Server 就是为开发调试设计的没有并发连接数限制策略没有慢请求超时机制没有连接复用优化也没有对恶意 User-Agent 的过滤能力。把它直接暴露在公网等于把家门钥匙挂在门口。这个流程适合谁适合所有正在把个人项目、团队内部工具、SaaS 原型、API 中间件、微服务节点从本地或测试环境推向真实用户的开发者。它不假设你懂 DevOps但要求你尊重生产环境的基本契约可观察、可恢复、可防御、可审计。如果你还在用forever或nodemon上生产或者用screen把进程挂后台那这篇内容就是为你写的。它不讲理论只讲我在 Ubuntu 22.04 上部署过 37 个不同规模 Node.js 服务后沉淀下来的、经过线上流量验证的、每一步都踩过坑的实操手册。2. 整体架构设计与方案选型逻辑2.1 为什么必须用 PM2 而不是 systemd 直接管理很多人会问Ubuntu 22.04 原生支持 systemd为啥不直接写个.service文件让 systemd 管理 Node.js 进程答案是可以但不推荐作为首选。systemd 确实能做进程守护、自动重启、日志收集但它对 Node.js 这类动态语言应用的生命周期管理过于“粗粒度”。举个典型场景你的应用在启动时需要连接 MongoDB而 MongoDB 服务启动稍慢systemd 默认的Aftermongod.service并不能保证 MongoDB 已完成初始化并接受连接。结果就是 Node.js 进程启动失败systemd 尝试重启又失败形成“启动风暴”日志刷屏。PM2 内置的--wait-for-app和--watch机制能监听应用内部的ready事件或特定端口的健康检查响应这才是真正的“应用就绪”判断。更重要的是PM2 提供了pm2 start ecosystem.config.js这种声明式配置把环境变量、工作目录、日志路径、集群数量、内存上限等全部集中管理。而 systemd 的.service文件每个参数都要手写Environment,WorkingDirectory,RestartSec一旦你要同时部署多个 Node.js 服务比如 API Admin Worker就得维护一堆.service文件极易出错。PM2 的pm2 list、pm2 show app、pm2 monit提供了统一的、可视化的操作入口这是 systemdjournalctl -u xxx永远无法替代的体验。当然PM2 本身也需要被 systemd 管理——我们最终会让 systemd 启动 PM2 进程并确保它随系统启动。这是一种“分层守护”systemd 守护 PM2PM2 守护 Node.js。这样既利用了系统级的稳定性又保留了应用级的灵活性。2.2 为什么 Nginx 是不可替代的反向代理层有人尝试用 Node.js 自己实现反向代理比如http-proxy-middleware或express-http-proxy。这在开发阶段没问题但生产环境绝对不行。原因有三第一性能损耗。Node.js 是单线程事件循环处理大量并发的 HTTP 请求、SSL 握手、Gzip 压缩、静态文件读取会严重挤占主应用的 CPU 时间片导致 API 响应变慢甚至超时。第二安全风险。自己写的代理逻辑很难覆盖所有 HTTP 协议边界情况比如 HTTP 请求走私HTTP Request Smuggling、慢速攻击Slowloris、恶意头注入等Nginx 经过十几年的互联网实战打磨这些防护是开箱即用的。第三功能缺失。Nginx 原生支持 OCSP Stapling加速 HTTPS 握手、Brotli 压缩比 Gzip 更高效、HTTP/2 Server Push预加载关键资源、GeoIP 地理位置路由、JWT 认证校验等高级特性这些用 Node.js 实现成本极高且稳定性无法保障。Nginx 在这里扮演的角色远不止“转发请求”这么简单。它是流量的第一道闸门。我们会在 Nginx 配置中强制开启HSTSHTTP Strict Transport Security让浏览器强制走 HTTPS设置X-Frame-Options和X-Content-Type-Options头防止点击劫持和 MIME 类型混淆攻击通过limit_req模块对/login接口做速率限制防暴力破解用map指令根据$http_user_agent屏蔽已知的爬虫或扫描器 UA。这些都不是 Node.js 应用该操心的事它们属于基础设施层的责任。把安全、性能、协议优化这些“脏活累活”交给 NginxNode.js 才能专注在业务逻辑上这才是合理的职责分离。2.3 为什么选择 NodeSource APT 仓库而非官方二进制包Node.js 官网提供 Linux 二进制包.tar.xz解压即可用。但生产环境追求的是“可重复、可审计、可升级”。APT 仓库的优势在于第一版本锁定。apt install nodejs18.19.0~dfsg1-1ubuntu1~22.04.1可以精确指定版本号避免apt upgrade时意外升级到不兼容的新版。第二依赖管理。APT 会自动安装nodejs所需的libuv1,libc-ares2,libnghttp2-14等底层库并确保版本匹配手动解压二进制包则需要自己解决这些依赖。第三安全更新。Ubuntu 官方安全团队会对 NodeSource 仓库中的包进行漏洞扫描和补丁推送你只需apt update apt upgrade就能获得修复。而手动安装的二进制包每次更新都要重新下载、校验、替换极易遗漏。我们选用 NodeSource 的setup_lts.x脚本是因为它指向的是当前 Active LTS 版本如 v18.x这是 Node.js 官方推荐用于生产环境的版本拥有 30 个月的长期支持周期比 Current 版本v20.x更稳定比旧版v16.x更安全。2.4 为什么必须创建专用的非 root 用户这是生产环境最基础、也最容易被忽视的安全红线。Ubuntu 22.04 默认的root用户拥有系统最高权限任何以 root 身份运行的进程一旦存在远程代码执行RCE漏洞攻击者就能直接获得整个服务器的控制权。而 Node.js 应用尤其是使用了child_process.exec、eval()、或解析不受信任的 YAML/JSON 配置的场景天然存在 RCE 风险。因此我们必须遵循“最小权限原则”为每个应用创建独立的系统用户该用户仅对应用所需目录如/var/www/myapp有读写权限对/etc、/usr、/root等敏感路径完全无权访问。这个用户不能是www-dataNginx 默认用户因为www-data需要读取 Nginx 配置和证书权限范围过大。我们创建一个专属用户比如myappuser并将其加入www-data组以便 Nginx 能读取应用生成的静态文件但绝不赋予sudo权限。所有后续操作——安装 Node.js、克隆代码、启动 PM2、配置 Nginx——都必须在这个用户上下文中完成。这不仅是安全要求更是运维规范。当多个团队共用一台服务器时清晰的用户隔离能让故障排查、权限审计、资源配额变得无比简单。我曾处理过一个事故某团队用 root 部署了一个 Node.js 服务其日志文件权限为644结果另一个团队的低权限用户误删了日志导致无法追溯问题根源。专用用户就是给每个应用划出一条清晰的“责任边界”。3. 核心细节解析与实操要点3.1 创建生产用户与权限体系第一步永远是“筑墙”而不是“建房”。我们先创建一个名为myappuser的专用用户并禁用其密码登录强制使用 SSH 密钥这是加固的第一步。# 以 root 或具有 sudo 权限的用户执行 sudo adduser --disabled-password --gecos myappuser # 将其加入 www-data 组以便 Nginx 能读取应用生成的静态文件 sudo usermod -a -G www-data myappuser # 创建应用主目录并设置所有权 sudo mkdir -p /var/www/myapp sudo chown -R myappuser:www-data /var/www/myapp sudo chmod -R 755 /var/www/myapp提示--disabled-password参数会创建一个没有密码的用户只能通过 SSH 密钥登录。这是生产环境的黄金标准。如果你必须使用密码请务必配合faillog和pam_faillock进行登录失败锁定。接下来切换到新用户并为其配置一个安全的 Shell 环境。我们不使用默认的/bin/bash而是用/bin/shdash因为它更轻量、启动更快、攻击面更小对于一个只运行 Node.js 的用户来说完全够用。# 切换用户 sudo su - myappuser # 修改默认 Shell chsh -s /bin/sh # 验证 echo $SHELL # 应输出 /bin/sh现在我们需要为这个用户配置 Node.js 的安装路径。我们不将 Node.js 安装到/usr/local那是系统级路径需要 root 权限而是采用“用户级安装”策略将 Node.js 二进制和全局模块安装到用户主目录下的~/.local中。这完全规避了权限问题且便于单用户管理。# 退出当前用户回到 root exit # 以 myappuser 身份执行 NodeSource 安装脚本 sudo -u myappuser bash -c curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - # 注意这里必须用 sudo -u而不是 su -因为 su - 会重置环境变量导致 curl 命令找不到 # 然后安装 Node.js 和 npm sudo -u myappuser apt-get install -y nodejs # 验证安装 sudo -u myappuser node --version # 应输出 v18.x.x sudo -u myappuser npm --version # 应输出 9.x.x注意apt-get install默认需要 root 权限所以我们用sudo -u myappuser来模拟该用户执行命令APT 会自动将包安装到系统路径/usr/bin/nodejs但npm全局安装的模块如pm2会默认放在~/.local/lib/node_modules下这正是我们想要的。3.2 使用 NVM 进行 Node.js 版本管理备选方案虽然我们推荐 APT 方式但如果你的应用需要频繁切换 Node.js 版本例如同时维护多个使用不同 Node 版本的微服务那么 NVMNode Version Manager是更好的选择。NVM 的核心优势在于“用户级沙盒”每个用户可以独立管理自己的 Node.js 版本互不干扰。# 以 myappuser 身份安装 NVM curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 重新加载 shell 配置 source ~/.bashrc # 安装并使用 LTS 版本 nvm install --lts nvm use --lts # 验证 node --version # v18.x.x npm --version # 9.x.xNVM 的安装路径是~/.nvm所有 Node.js 版本都存放在其中nvm use会修改当前 shell 的PATH环境变量使其优先找到~/.nvm/versions/node/v18.x.x/bin下的node和npm。这种方式下npm install -g pm2会将pm2二进制安装到~/.nvm/versions/node/v18.x.x/bin/pm2完全在用户空间内无需任何sudo。这对于 CI/CD 流水线或容器化部署尤其友好。3.3 PM2 的深度配置与生态文件PM2 的灵魂在于ecosystem.config.js。这是一个 JavaScript 配置文件它定义了应用的“生命契约”。我们不会用pm2 start app.js这种命令行方式因为那无法持久化配置。// /var/www/myapp/ecosystem.config.js module.exports { apps: [{ name: myapp-api, // 应用名称显示在 pm2 list 中 script: ./src/index.js, // 入口文件路径相对于 cwd cwd: /var/www/myapp, // 工作目录 args: , // 启动参数如 --port 3001 interpreter: /usr/bin/node, // 显式指定 node 解释器路径避免 PM2 自动探测错误 interpreter_args: , // 解释器参数如 --trace-warnings watch: [src, config], // 监听哪些目录变化触发重启 ignore_watch: [node_modules, logs, .git], // 忽略监听的目录 max_memory_restart: 512M, // 内存超过 512MB 自动重启防内存泄漏 instances: 2, // 启动 2 个实例利用多核 CPU exec_mode: cluster, // 集群模式比 fork 模式更高效 env: { NODE_ENV: development, PORT: 3000, DATABASE_URL: mongodb://localhost:27017/myapp_dev }, env_production: { NODE_ENV: production, PORT: 3000, DATABASE_URL: mongodb://127.0.0.1:27017/myapp_prod, LOG_LEVEL: warn } }], deploy: { production: { user: myappuser, host: your-server-ip, ref: origin/main, repo: https://github.com/yourname/myapp.git, path: /var/www/myapp, post-deploy: npm install npm run build pm2 reload ecosystem.config.js --env production } } };这个配置文件包含了生产部署所需的一切。instances: 2和exec_mode: cluster是关键它让 PM2 启动一个主进程Master和两个工作进程Worker主进程负责负载均衡和健康检查工作进程处理实际请求。当一个 Worker 因异常崩溃时Master 会立即拉起一个新的 Worker用户几乎感知不到中断。max_memory_restart是一道保险Node.js 的 V8 引擎在长时间运行后可能出现内存碎片导致 RSSResident Set Size持续增长这个参数能强制回收。实操心得interpreter字段必须显式指定Ubuntu 22.04 上APT 安装的 Node.js 二进制在/usr/bin/node而 NVM 安装的在~/.nvm/versions/node/v18.x.x/bin/node。如果不指定PM2 可能在不同环境下探测到不同的node导致require()路径错误或process.env丢失。cwd字段也至关重要它决定了require(./config/db)这样的相对路径从哪里开始解析必须和你的代码结构严格匹配。3.4 Nginx 配置的精细化打磨Nginx 配置不是“复制粘贴”就能用的。一个健壮的生产配置需要考虑 SSL、缓存、安全头、错误处理等多个维度。# /etc/nginx/sites-available/myapp upstream myapp_backend { server 127.0.0.1:3000; # 如果你用了 PM2 cluster 模式可以加多个 server实现负载均衡 # server 127.0.0.1:3001; # server 127.0.0.1:3002; } server { listen 80; server_name myapp.example.com; # 强制 HTTP 重定向到 HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name myapp.example.com; # SSL 证书和密钥 ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/myapp.example.com/chain.pem; # SSL 性能优化 ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; # 安全头 add_header X-Frame-Options DENY always; add_header X-XSS-Protection 1; modeblock always; add_header X-Content-Type-Options nosniff always; add_header Referrer-Policy no-referrer-when-downgrade always; add_header Content-Security-Policy default-src self; script-src self unsafe-inline unsafe-eval; style-src self unsafe-inline; img-src self data:; font-src self; connect-src self; frame-src none; always; # 日志 access_log /var/log/nginx/myapp_access.log; error_log /var/log/nginx/myapp_error.log; # 静态文件处理 location /static/ { alias /var/www/myapp/public/static/; expires 1y; add_header Cache-Control public, immutable; } # API 请求反向代理 location /api/ { proxy_pass http://myapp_backend/; 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_set_header X-Nginx-Proxy true; proxy_redirect off; proxy_buffering on; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; } # 前端 SPA 的 history 模式支持 location / { try_files $uri $uri/ /index.html; } # 健康检查端点 location /health { return 200 OK; add_header Content-Type text/plain; } }提示proxy_pass http://myapp_backend/;结尾的/至关重要。它表示“去除匹配的前缀再转发”。例如/api/users会被转发为http://myapp_backend/users。如果漏掉/就会变成http://myapp_backend/api/users而后端 Node.js 应用可能根本没注册/api/api/users这个路由导致 404。这个配置中add_header Content-Security-Policy是一道强大的防线它告诉浏览器“只允许从我的域名加载脚本、样式、图片禁止内联脚本unsafe-inline和eval()unsafe-eval”。虽然它不能阻止所有 XSS但能极大增加攻击难度。expires 1y和Cache-Control则是前端性能的关键让浏览器缓存静态资源一年大幅减少网络请求。4. 实操过程与核心环节实现4.1 从零开始完整部署流程现在我们把所有步骤串起来形成一个可重复执行的流水线。请严格按照顺序操作每一步都有其不可跳过的理由。步骤 1服务器初始化与防火墙配置# 以 root 登录 # 更新系统 apt update apt upgrade -y # 安装并启用 UFWUncomplicated Firewall apt install -y ufw ufw default deny incoming ufw default allow outgoing ufw allow OpenSSH ufw allow Nginx Full # 这会自动开放 80 和 443 ufw enable # 验证 ufw status verboseUFW 是 Ubuntu 的防火墙前端它比直接操作iptables更安全、更易用。“默认拒绝入站”是铁律。我们只明确开放 SSH22端口和 Nginx80/443端口其他所有端口包括 Node.js 默认的 3000一律关闭。这是生产环境的第一道物理屏障。步骤 2创建用户与安装 Node.js# 创建用户 adduser --disabled-password --gecos myappuser usermod -a -G www-data myappuser mkdir -p /var/www/myapp chown -R myappuser:www-data /var/www/myapp chmod -R 755 /var/www/myapp # 安装 Node.js (APT 方式) curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - apt-get install -y nodejs # 切换到用户验证 sudo -u myappuser -i node --version npm --version步骤 3部署应用代码# 仍在 myappuser 下 cd /var/www/myapp # 克隆你的代码库请替换为你的实际地址 git clone https://github.com/yourname/myapp.git . # 安装依赖 npm install --production # --production 只安装 dependencies跳过 devDependencies # 创建 PM2 配置文件 nano ecosystem.config.js # ... 粘贴上面的配置 ... # 启动应用 pm2 start ecosystem.config.js --env production # 保存当前进程列表以便服务器重启后自动恢复 pm2 save # 设置 PM2 开机自启 pm2 startup systemd -u myappuser --hp /home/myappuser # 这会输出一条命令复制并执行它 # 例如sudo env PATH$PATH:/home/myappuser/.nvm/versions/node/v18.x.x/bin /home/myappuser/.nvm/versions/node/v18.x.x/lib/node_modules/pm2/bin/pm2 startup systemd -u myappuser --hp /home/myappuserpm2 startup命令会生成一个 systemd service 文件/etc/systemd/system/pm2-myappuser.service并启用它。这意味着无论服务器因断电还是维护重启PM2 进程都会自动拉起进而拉起你的 Node.js 应用。pm2 save则会将当前所有进程的状态名称、脚本、环境变量等保存到~/.pm2/dump.pm2文件中pm2 startup生成的 service 会读取这个文件来恢复。步骤 4安装与配置 Nginx# 退出 myappuser回到 root exit # 安装 Nginx apt install -y nginx # 禁用默认站点 rm /etc/nginx/sites-enabled/default # 创建我们的站点配置 ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp # 测试配置语法 nginx -t # 如果输出 syntax is ok, test is successful则重载 Nginx systemctl reload nginx步骤 5获取并配置 SSL 证书我们使用 Lets Encrypt 的 Certbot 工具它能免费、自动化地获取和续期证书。# 安装 Certbot apt install -y certbot python3-certbot-nginx # 获取证书请将 myapp.example.com 替换为你的域名 certbot --nginx -d myapp.example.com # Certbot 会自动修改你的 Nginx 配置添加 SSL 相关指令并重载 Nginx # 验证证书是否生效 openssl s_client -connect myapp.example.com:443 -servername myapp.example.com 2/dev/null | openssl x509 -noout -datesCertbot 会自动为你配置好ssl_certificate和ssl_certificate_key并设置好自动续期的 systemd timercertbot.timer。你无需手动干预。步骤 6最终验证与监控一切就绪后进行最终验证# 检查 PM2 进程状态 sudo -u myappuser pm2 list # 检查 Nginx 状态 systemctl status nginx # 检查应用是否在监听 3000 端口应该只有 127.0.0.1而非 0.0.0.0 ss -tuln | grep :3000 # 从外部访问你的域名看是否能正常打开 curl -I https://myapp.example.com # 检查健康端点 curl https://myapp.example.com/healthss -tuln | grep :3000这条命令至关重要。它应该只显示127.0.0.1:3000而不是*:3000或0.0.0.0:3000。这证明 Node.js 进程只绑定在本地回环地址外部网络无法直接访问所有流量都必须经过 Nginx这是架构安全的基石。4.2 关键参数计算与选择依据PM2 的instances数量如何确定这不是拍脑袋决定的。一个经验公式是instances CPU 核心数 * 1.5。例如你的服务器是 4 核 CPU那么instances: 6是一个合理起点。但必须结合应用特性调整。如果你的应用是 I/O 密集型大量数据库查询、API 调用instances可以设得更高因为 Node.js 的事件循环在等待 I/O 时是空闲的可以处理更多并发。如果你的应用是 CPU 密集型大量图像处理、加密计算instances应该等于 CPU 核心数避免过多的上下文切换开销。PM2 的pm2 monit命令会实时显示每个 Worker 的 CPU 和内存占用你可以根据这个数据动态调整。Nginx 的proxy_buffer_size和proxy_buffers如何设置这取决于你的后端响应体大小。proxy_buffer_size用于存储响应头通常 128k 足够。proxy_buffers用于存储响应体格式是proxy_buffers number size。number是缓冲区的数量size是每个缓冲区的大小。一个保守的设置是proxy_buffers 4 256k意味着最多可以缓存 1MB 的响应体。如果后端返回一个 2MB 的 JSON 数据而缓冲区不够Nginx 就会将数据写入临时文件proxy_temp_path这会显著降低性能。你可以用abApache Bench工具压测你的 API观察 Nginx 的proxy_temp目录是否有文件生成如果有就需要增大缓冲区。SSL 的ssl_session_cache大小如何估算ssl_session_cache shared:SSL:10m表示创建一个 10MB 的共享内存区域来缓存 SSL 会话。一个典型的 SSL 会话缓存条目大约占用 1KB 内存。所以 10MB 可以缓存约 10,000 个会话。对于一个 QPS每秒查询数为 100 的网站如果平均会话超时是 10 分钟600 秒那么理论上最多有100 * 600 60,000个活跃会话。此时10MB 就不够了你需要shared:SSL:60m。ssl_session_timeout 10m是一个平衡点太短会导致频繁握手太长会占用过多内存。4.3 实操现场记录一次典型的部署排错上周我帮一个客户部署一个 Vue3 Node.js 的商城后台。所有步骤都按本文执行但在curl https://myapp.example.com时返回了502 Bad Gateway。这是 Nginx 的经典错误意味着它无法成功连接到后端myapp_backend。排查思路检查 Nginx 错误日志tail -f /var/log/nginx/myapp_error.logconnect() failed (111: Connection refused) while connecting to upstream检查后端是否在监听ss -tuln | grep :3000没有任何输出Node.js 进程根本没起来。检查 PM2 状态sudo -u myappuser pm2 list发现myapp-api的状态是errored。查看 PM2 日志sudo -u myappuser pm2 logs myapp-apiError: Cannot find module /var/www/myapp/src/index.js问题定位了ecosystem.config.js中的script: ./src/index.js路径错了。客户的代码结构是/var/www/myapp/server/index.js而不是/var/www/myapp/src/index.js。这是一个典型的路径配置错误。解决方案修改ecosystem.config.js将script字段改为./server/index.js然后执行sudo -u myappuser pm2 reload ecosystem.config.js --env productionpm2 reload会优雅地重启所有进程先启动新的 Worker等它准备好接收请求后再关闭旧的 Worker实现零停机更新。这比pm2 restart更安全。实操心得永远不要相信“路径看起来是对的”。在pm2 start之前先用sudo -u myappuser node ./src/index.js手动运行一次看是否能成功启动并监听端口。这是最快速的验证方式。5. 常见问题与排查技巧实录5.1 “Connection refused” 与端口监听问题这是部署中最常遇到的错误表现形式多样Nginx 502、curl: (7) Failed to connect、net::ERR_CONNECTION_REFUSED。核心原因只有一个Node.js 进程没有在预期的 IP 和端口上监听。排查清单| 检查项 | 命令 | 预期输出