Node.js 生产环境部署到 VPS:PM2 + Nginx 反代 + 安全基线
2026-08-15 · DevCraft Studio
本地 npm run dev 可不行。讲清用 PM2 守护进程、Nginx 反代、环境变量管理与非 root 运行这些生产级动作。
你本地 npm run dev 跑得好好的接口,一旦丢到 VPS 上直连 3000 端口,问题马上就来了:终端一关进程就死、崩溃了没人拉起、服务器重启得手动起、WebSocket 半夜断了没人管。开发环境和生产环境之间的这道坎,表面上看是“怎么把代码跑起来”,实际上是“怎么让代码在生产里活得久、跑得稳、出事能自愈”。这篇文章把 Node.js 上线到 VPS 这件事拆成四块:进程守护(PM2)、反向代理(Nginx)、密钥管理(.env)和最小权限(非 root + 防火墙)。每块都给可直接抄的命令,照着做一遍,你的服务就能从“能跑”变成“能扛”。
延伸阅读
更多相关攻略推荐:从 Git Push 到秒级上线:CI/CD 流水线与"无中断"发布、【VPS 进阶玩法精选 030】用 VPS 搭建云端 IDE(cod、连云厂商也看不到你的数据?机密计算(Confidential Com、被割裂的"云上互联网":数据主权(GDPR)、本地化存储与主权云(S、什么时候非得要独立 IPv4?便宜国外 VPS 独立 IP 适用场景。
一、为什么 npm run dev 只是个开发玩具
开发时 node server.js 或 npm run dev 之所以省心,是因为它把日志直接打到终端、改代码热重载、崩了你就手动重启。但生产环境要的是相反的三件事:进程退出要自动拉起、机器重启要自动上线、一份代码要榨干所有 CPU 核心。npm run dev 这三件一件都做不到。更隐蔽的坑是,开发服务器往往开着调试接口、打印完整堆栈、默认监听 0.0.0.0 且没有任何 TLS——这些在本地无所谓,放到公网就是直接送分。所以上线的第一步不是“怎么跑”,而是“用什么跑”。对单机 VPS 来说,PM2 是目前最轻、最稳、学习成本最低的进程管理器,没有之一。
还有一个常见误区是以为“能访问就行”。开发服务器往往把错误堆栈、依赖版本、内部路径直接打印在响应里,一旦暴露到公网,攻击者拿这些信息就能精准定位漏洞。生产里 NODE_ENV=production 会关掉这些调试输出,但前提是你的进程真跑在生产模式下,而 PM2 的 env_production 正是把这个开关固化下来的地方。换句话说,从开发切到生产,不只是换条启动命令,而是换一整套运行假设:默认拒绝、最小权限、可观测、可自愈。
二、PM2:让进程自己活下来,还能开机自启
PM2 的本质是一个常驻的进程管理器:你交给它一个入口文件,它负责拉起、看守、重启、记录日志,还能用 cluster 模式按 CPU 核心数开多个实例。先全局装上它(建议装在非 root 的专用用户下):
npm install -g pm2
pm2 start dist/server.js --name my-api --env production
pm2 startup
pm2 save其中 pm2 startup 会生成一段 systemd 集成命令,按提示用 root 跑一次,之后服务器重启 PM2 会自动把保存过的进程列表拉起来;pm2 save 则是把“当前要跑哪些进程”固化下来,否则重启后 PM2 不记得要复活谁。生产环境别在命令行里堆 flag,用一个 ecosystem 文件更清晰,也方便回滚:
module.exports = {
apps: [{
name: 'my-api',
script: 'dist/server.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '512M',
watch: false,
env_production: {
NODE_ENV: 'production',
PORT: 3000
}
}]
};这里 instances: 'max' 加 exec_mode: 'cluster' 等于“一个端口、多个进程、自动负载均衡”,不用改一行业务代码就能把多核用满;max_memory_restart 是兜底,某个 worker 内存涨过 512M 就自动重启,专治内存泄漏。日常命令记这几个就够:pm2 list 看状态、pm2 logs my-api 看日志、pm2 reload my-api 做零停机发布(cluster 模式下逐个换 worker,连接不丢)、pm2 monit 看实时 CPU 和内存。日志久了会撑爆磁盘,装个 pm2-logrotate 设个 50M 滚动、保留 7 天,比哪天磁盘写满导致全线 502 强得多。
三、Nginx 反代:把 TLS、静态资源和 WebSocket 都接住
Node 不该直接裸奔在 80/443 上。让它乖乖监听 127.0.0.1:3000,前面放一层 Nginx 做反向代理,好处是一整片:TLS termination(证书只在 Nginx 配一次)、gzip 压缩、静态文件直出、客户端慢连缓冲、以及最重要的——WebSocket 升级。没有下面这两行 Upgrade/Connection 头,你的 Socket.IO、实时推送会握手失败,表现就是“开发环境好好的,一上线长连接就断”:
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
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;
}
}写好配置后 ln -s 到 sites-enabled,先 nginx -t 测语法,再 systemctl reload nginx。静态资源(图片、打包后的前端 assets)单独用一个 location 交给 Nginx 直出并设长缓存,比让 Node 去读磁盘快一个量级。证书用 certbot --nginx 一把梭,自动续期定时器也顺带装好。proxy_read_timeout 默认 60 秒,如果你的接口有长轮询或大文件流,适当调大到 120 秒,否则 Nginx 会先你一步断开连接。
四、.env 与密钥:配置和代码分开,密钥绝不进仓库
把数据库密码、JWT 密钥、第三方 API key 硬编码进源码,是这个行业最常见的事故源头——它们会永远留在 git 历史里,哪怕你后来删了也还在。正确做法是:配置通过环境变量注入,.env 只存在于服务器本地,且 .gitignore 里必须有一行 .env。Node 侧用 dotenv 读取,或者 Node 20.6+ 直接用 node --env-file=.env app.js 原生加载,省一个依赖:
require('dotenv').config();
const dbUri = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) throw new Error('JWT_SECRET is not set');本地提交一个 .env.example,里面全是占位符,告诉后来人“需要哪些变量”,但绝不写真值。更稳的做法是用 Zod 或 Joi 在启动那一刻校验环境变量:缺了关键的就直接退出,而不是带着半套配置默默跑起来,半夜出了诡异的错才去猜。生产环境的 .env 权限设成 600,只让运行用户读。再往上一步才是 AWS Secrets Manager、Vault 这类托管方案——对小项目来说,一个 chmod 600 的 .env 加上不进仓库,已经挡掉了 90% 的泄密风险。
五、非 root 运行 + 防火墙:把爆炸半径压到最小
永远不要让 Node 以 root 身份跑。一旦进程被攻破,攻击者的起点就是 root,容器逃逸、提权、横向移动全都敞开了。建一个只够用的系统用户,把代码目录和 .env 的所有权交给它,再用 sudo -u 启动 PM2:
sudo adduser --system --group --home /opt/nodeapp nodeapp
sudo chown -R nodeapp:nodeapp /opt/nodeapp
sudo -u nodeapp pm2 start ecosystem.config.js --env production配合防火墙基线,只放行真正需要的端口。用 ufw 的话,原则就是“默认拒绝、按需放开”:SSH(建议改非 22 端口并只用密钥)、Nginx 的 80/443,其余一律关掉。Node 监听的是 127.0.0.1,外网根本连不进来,攻击者连敲门的机会都没有:
sudo ufw default deny incoming
sudo ufw allow 22
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status最后别忘了两条软基线:NODE_ENV 必须设为 production(Express 会因此关掉调试接口、不吐堆栈、走优化路径),以及应用自身要处理 SIGTERM 做优雅停机——关进程前先把在途请求跑完、把数据库连接关干净,这样 pm2 reload 才是真正的零停机,而不是“杀完再起”。
如果你的 VPS 只有 1 核或 2 核(很多入门套餐就是这样),cluster 模式开 instances: 'max' 也只会有 1 到 2 个 worker,这时候别指望靠堆进程提吞吐,应该把精力放在减少每个请求的开销上:开 gzip、加缓存、把慢查询挪到后台任务。PM2 的 monit 面板能实时看到每个 worker 的 CPU 和内存占用,上线头几天多瞄两眼,往往能提前发现内存泄漏或死循环,比等告警响了再救火从容得多。发布更新也可以写成一个 deploy.sh:拉代码、npm ci、pm2 reload,一条命令完成零停机上线,避免每次手动敲一串还敲错。
把上面五块串起来,一条完整的上线链路就是:非 root 用户拉代码 → npm ci 装依赖 → pm2 reload 热更新 → Nginx 在前面接公网流量和 TLS → ufw 只留必要端口。这比起裸跑 node,稳定性提升不是一星半点,而成本只是多写两个配置文件。
延伸阅读:VPS 新手入门指南 讲清买完机器第一步做什么;VPS 安全基线 把防火墙、SSH 密钥与 fail2ban 一次讲透;用 Traefik 做反向代理 是 Nginx 之外的另一种自动 HTTPS 方案。