ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Next.js部署上云全攻略:pm2进程守护与Nginx反向代理实战

Next.js部署上云全攻略:pm2进程守护与Nginx反向代理实战

"PM2:单实例多进程,应用无需改动即可提升并发,自带日志和监控面板;"以此为引子,展开Next.js项目部署的完整过程。

Next.js服务端渲染项目部署上云:从本地到阿里云服务器,我用pm2守护了整整48小时

把Next.js这种服务端渲染框架的项目部署到云服务器,听起来像是"npm run build完事"的简单操作,但真上手做一遍,你会发现坑比想象中多得多。端口、环境变量、Node版本、内存占用、开机自启,任何一个环节没处理好,线上项目就会在半夜给你上演"死给你看"的戏码。这篇文章就围绕我从零开始把Next.js服务端渲染框架部署到一台阿里云服务器的完整过程,重点记录pm2进程守护的配置细节和我在这个过程中踩过的坑。

不管你是刚学会Next.js基础、准备把自己写的博客或后台管理系统上线,还是已经部署过一次但被各种玄学问题搞得头疼,这篇文章应该都能帮到你。我会尽量把每一步"为什么这么做"讲清楚,而不是甩给你一串复制粘贴的命令。

1. 为什么Next.js部署比传统React项目更"娇贵"

先说结论:Next.js因为带了服务端渲染(SSR)能力,它不只是把静态资源扔到服务器上让人下载就行,而是需要在服务器上真实运行一个Node.js服务。

1.1 从Nginx托管静态文件到Node常驻进程的本质变化

以前部署Create React App这类纯前端项目,流程很无脑:npm run build生成一堆静态文件,扔到Nginx的root目录,配置个反向代理转发API请求就完事了。用户访问页面,Nginx直接返回HTML和JS资源,服务器本身只是"仓库管理员"。

Next.js不一样。当用户请求一个页面时,如果这个页面用了getServerSideProps或者getInitialProps,服务器需要在收到请求的那一瞬间,去调用后端接口、组合数据、渲染出完整的HTML再返回给浏览器。注意:这个渲染动作是发生在服务端的、是动态的。所以你必须在服务器上让一个Node进程一直处于运行状态,它得24小时待命,随时准备处理请求。

这也是为什么很多第一次部署Next.js的人会困惑:我明明npm run build成功了,把文件传上去,Nginx也指向了.next文件夹,怎么页面打开一片空白?因为.next里的文件不是给你当作静态资源直接访问的,它需要由Next.js的server运行时来读取和响应。

1.2 "部署"实际上是在跑一个什么角色

本地开发时,你执行npm run dev,那是开发模式的server,热更新重编译,性能开销大。部署到生产环境,你要跑的是next start,它启动的是经过编译后的生产模式服务。这个服务本质上就是一个Node.js的HTTP服务进程,默认监听3000端口。

打个比方:Next.js构建产物像是做好了一桌子半成品菜,next start就是厨师本人站在灶台前,客人(用户请求)来了,他现场开火炒菜端出来。Nginx在这些场景里更像是一个"迎宾员",负责把80端口的客人引导到3000端口找厨师。你不可能让迎宾员把半成品菜直接端给客人。

2. 服务器环境准备工作:每一步都有讲究

2.1 选服务器配置时的"够用"标准

我用来部署的是一台2核4G的阿里云ECS,带宽5M。先说明白一件事:很多人问"Next.js项目到底要多大服务器才跑得动",这个问题的答案完全取决于你的项目规模。

注意:纯静态页面多、访问量小的个人项目,2核2G足够;如果涉及大量服务端渲染、图片处理、内存缓存,建议直接上4G内存起步。Next.js的Node进程在空闲时大约占300MB-500MB内存,页面一多、并发一上来,内存开销会明显上涨,别在这上面抠门。

另外部署前一定要检查服务器系统盘剩余的容量。有一次我部署一个项目,node_modules加构建产物把原本就不富裕的40G系统盘塞满了,后来排查了半天才发现是磁盘满导致的各种诡异报错。执行df -h看一眼,简单省事。

2.2 Node版本管理:nvm是必要的

Next.js不同版本对Node版本的要求各有差异,比如较新的Next.js 14要求Node 18.17以上,Next.js 15要求Node 18.18或更高。直接用apt装的Node版本往往偏旧,所以建议在服务器上先装nvm(Node版本管理器)。

具体操作流程:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" nvm install 18.20.4 nvm use 18.20.4 nvm alias default 18.20.4

装完后执行node -v确认版本。注意要做到"default别名"这一步,否则下次重启服务器,node命令可能直接找不到了。

2.3 代码上服务器的三种主流姿势

  • Git拉取:最推荐的正式方式。服务器上装Git,把代码库clone下来,发版时git pull,简单可控。不过你得处理好仓库凭证,私有仓库建议用Deploy Keys。
  • rsync直传:适合小项目和快速验证。在本地执行rsync -avz --exclude node_modules --exclude .next ./ user@server:/path/to/project,增量传输,比scp好用得多。
  • 本地压缩上传解压:最粗暴,但容易出现文件权限、换行符问题。

我用的是第一种:在服务器上做一个项目专属目录,把仓库clone下来。这样以后改代码、发版本,直接在服务器拉取最新代码就行,配合Webhook自动化还可以做到"提交即发布"。

3. 构建与启动:别急着上pm2,先手动跑一遍

很多教程上来就让你"npm run start && pm2 start xxx",但我的建议是:先用最原始的方式把流程走通,再交给进程守护工具。这样出了问题你能分辨是程序错了还是守护工具配置错了。

3.1 生产构建,装依赖时注意"NODE_ENV"

在项目目录下执行:

npm ci --omit=dev

先解释一下--omit=dev:这个参数让 npm 只安装生产依赖,跳过devDependencies里那些构建用的工具包。Next.js的next build需要用到一些构建工具,所以合理的流程是:

# 1. 安装全部依赖(包含devDependencies,为了build) npm ci # 2. 执行生产构建 npm run build # 3. 重新安装依赖,去掉devDependencies(为了让服务器上更干净) npm ci --omit=dev # 4. 启动生产服务 npm run start

针对第2到第3步,很多人会疑惑:构建完再重新装依赖,不多此一举吗?其实Next.js的构建产物.next目录是自包含的,它已经把所有需要的模块打包好了,运行时并不依赖devDependencies里的构建工具。生产环境只装生产依赖,节省磁盘空间、降低体积,还减少了一些不必要的包带来的安全隐患。

3.2 next start默认端口调整与验证

默认情况下next start监听3000端口。如果你只跑一个项目,3000端口没毛病。但服务器上往往还有别的服务占着端口,我建议你直接改成自定义端口,比如把项目跑在3001上,避免和别的应用冲突。

在package.json的scripts里调整一下:

{ "scripts": { "start": "next start -p 3001" } }

然后手动启动验证:

npm run start

看到类似▲ Next.js 14.1.0和- Local: http://localhost:3001的输出,说明构建产物正常工作。用curl -I http://localhost:3001测试一下,能返回HTTP状态码200,就说明服务真的跑起来了。

注意:如果你在服务器上执行curl localhost:3001能通,但浏览器访问公网IP+端口打不开,先怀疑云服务器的安全组规则。阿里云的安全组需要在控制台单独开放端口,默认只开了22、80、443等少数端口,3001这种自定义端口需要在安全组里添加入方向规则。

3.3 第一次手动启动遇到的"血泪":内存不足导致进程被杀

我手动启动后不久,访问了几下页面,再回来pm2 list一看,进程没了。查系统日志发现是OOM Killer动手了。后来分析,问题出在两个地方:

服务器内存只有4G,而Next.js在生产模式下默认预编译了许多页面,加上Node进程内存管理机制,高峰期内存占用会突然飙升。另外我那个项目的页面里有些服务端数据处理逻辑写得不高效,导致响应缓慢、内存堆积。

临时解决办法是给Node进程设置内存上限:

NODE_OPTIONS="--max-old-space-size=2048" next start -p 3001

这会让Node的V8引擎在内存占用接近2G时主动进行更频繁的垃圾回收,而不是任由内存涨到物理上限被系统杀掉。

提示:这只是一个缓解方案,如果你的Node进程经常内存告警,一定要回到代码层面优化。特别是getServerSideProps里的数据请求,尽量加缓存、限制并发,别把服务端渲染当成无脑API调用层。

4. pm2进程守护配置:这是整篇文章的核心环节

手动跑通了,接下来就是让服务"死了能活、开机自启、日志有迹可循"。这个环节的主角是pm2。

4.1 为什么选pm2而不是forever或systemd

先聊一下背景。Node服务进程守护的常见方案有几种:

方案优点缺点
forever轻量、简单,对新手友好功能单一,无集群模式,日志管理弱
pm2内置负载均衡(cluster模式)、日志管理、监控面板、开机自启,生态成熟配置项多,文档稍显零散
systemdLinux原生的服务管理,稳定可靠没有进程崩溃自动恢复的精细化策略,配置繁琐

我见过不少人用forever,但它更大的价值在于"挂了重启",而pm2更像是"进程管家":你可以管理多个应用、查看每个应用的CPU/内存占用、日志实时滚动、一键开启重启策略。对于一个需要长期稳定对外提供服务的Next.js应用,pm2的cluster模式还能直接跑多个实例,最大化利用多核CPU。所以我的选型答案很明确:pm2。

4.2 安装pm2与第一个配置文件

全局安装pm2:

npm install -g pm2

安装完成后,建议把你自己的启动配置写成一个文件,这样以后重启项目、换服务器都不用重新敲命令。我习惯在项目根目录放一个ecosystem.config.js,内容如下:

module.exports = { apps: [ { name: 'my-next-app', script: 'node_modules/next/dist/bin/next', args: 'start -p 3001', cwd: '/var/www/my-next-app', instances: 1, exec_mode: 'fork', env: { NODE_ENV: 'production' }, max_memory_restart: '2G', out_file: '/var/log/my-next-app/out.log', error_file: '/var/log/my-next-app/error.log', merge_logs: true, log_date_format: 'YYYY-MM-DD HH:mm:ss' } ] };

说明几个关键配置:

  • script直接指向next的可执行入口,而不是npm run start。这让pm2绕过npm那一层外壳,直接管理Node进程,更稳。
  • args里带了端口参数,服务和启动命令一致,避免"改一处漏一处"。
  • instances: 1配合exec_mode: 'fork':fork模式单实例,适合大多数不需要多核扩展的场景;cluster模式可以填大于1的数值,但要注意如果应用内部有内存状态或定时任务,多实例反而会引发重复执行问题。
  • max_memory_restart: '2G':内存超过2G自动重启,呼应前面说的OOM问题。
  • 日志文件之前先创建好目录,否则pm2不会自动创建。

启动:

pm2 start ecosystem.config.js pm2 save

pm2 save会把当前进程列表保存为"开机启动清单"。之后执行pm2 startup,它会输出一句需要你在root权限下执行的命令,拷贝执行一下,就能在重启机器后自动拉起这些进程。

4.3 pm2进程守护的核心机制与常见误区

很多教程把pm2说得神乎其神,好像装了就万事大吉。但你需要明白pm2的守护逻辑:

它本质上是一个常驻的"看门人"进程机制。你的Next.js应用进程如果崩溃退出,pm2检测到后会按照配置的restart_delay、max_restarts等策略来拉起新进程。

默认配置下,如果应用在短时间内反复崩溃,pm2会进入"稳定模式",停止反复重启尝试。这是保护机制,防止服务器被无意义的循环重启拖死。所以如果看到Stable状态,别慌,那是它在告诉你:别再自动重启了,你得自己排查问题。

另外提一个容易踩的坑:pm2管理的是进程,不是端口冲突检测器。如果3001端口已经被其他程序占用,next start会启动失败,pm2会不停尝试拉起,日志里全是EADDRINUSE。解决方法:先lsof -i:3001确认端口占用,杀掉占用进程再重启应用。

4.4 常用运维命令清单

我把这段时间最常用的pm2命令整理一下,贴到服务器运维笔记里,你也随手存一份:

pm2 list # 查看所有应用状态 pm2 logs my-next-app # 实时查看日志 pm2 monitor # 打开终端监控面板 pm2 restart my-next-app # 重启应用 pm2 reload my-next-app # 平滑重载(cluster模式专用) pm2 stop my-next-app # 停止应用 pm2 delete my-next-app # 从管理列表移除 pm2 startup # 配置开机自启 pm2 save # 保存当前进程树

记得每次改完ecosystem.config.js后执行pm2 restart my-next-app --update-env,pm2才会重新加载新的环境变量和配置。

5. Nginx反向代理配置:让Next.js服务从"内网"变成"公网"

5.1 从端口直开到代理隐藏的思维转变

服务跑在3001端口,安全组开放了3001,公网IP加端口也能访问。但这带来几个问题:访问不够优雅、暴露了应用端口、SSL证书只能配在单独的证书端口上别扭得很。所以正规做法是:让Nginx监听80/443端口,把请求转发到3001端口。

用户访问域名,Nginx接收请求,根据配置把请求转发给Next.js的Node服务处理,再将响应返回给用户。这里的核心配置是proxy_pass:

server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:3001; 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; } }

5.2 几组重要header的详细解释

proxy_set_header Host $host:把客户端访问的域名原样传给后端。如果没有这一行,Next.js里如果用了基于req.headers.host的逻辑(比如生成绝对链接、跨域cookie判断)就会出错。

proxy_set_header X-Forwarded-For:让后端能看到客户端的真实IP。启用后,在getServerSideProps中可以通过分析X-Forwarded-For来获取访客IP,做埋点、限流都离不开它。

proxy_set_header X-Forwarded-Proto $scheme:表示客户端用的是HTTP还是HTTPS。Next.js如果检测到是HTTPS请求,生成的一些资源链接才是https://,否则即使你实际上了HTTPS,页面里的资源也可能被写成http://,造成混合内容警告。

注意:如果配置了HTTPS,还需要让Next.js知道它运行在代理后面,否则重定向逻辑和cookie的secure属性会有偏差。可以在启动命令里加上-H 'X-Forwarded-Proto: https'或者在应用代码里识别x-forwarded-proto头。

5.3 静态资源的缓存策略

Next.js的静态资源(/_next/static/路径下的JS、CSS、图片)默认由Next.js服务自己处理。Nginx层面可以针对这一路径加更激进的缓存,减轻Node服务的重复处理压力:

location /_next/static/ { alias /var/www/my-next-app/.next/static/; expires 365d; access_log off; }

当然,如果你的.next目录不在Nginx的访问路径下,更简单的做法是不用alias,直接让请求转发到后端,借助Next.js自己响应里的Cache-Control头即可。我实际环境的做法是:静态资源走CDN,源站回源到域名,缓存策略交给CDN厂商控制,省心省力。

5.4 HTTPS证书:let's encrypt一键配置

因为有了Nginx这层反向代理,HTTPS配置变得非常简单。用certbot之类的工具签发证书,它会自动修改Nginx配置并处理续期。我用的命令大致是:

apt install nginx certbot python3-certbot-nginx certbot --nginx -d example.com

certbot会自动完成证书申请、Nginx配置注入和自动续期定时任务。需要注意的是,如果之后手动改了Nginx配置,重新执行一次certbot --nginx -d example.com让它重新检测即可。

6. 部署中踩过的坑:那些不会写在官方文档里的细节

6.1 环境变量"消失"了

本地开发时.env.local里的环境变量,上传到服务器后没生效。排查了半天,发现是NODE_ENV=production时Next.js默认加载.env.production而不是.env.local。

解决方案:在服务器上创建对应的.env.production.local文件(Next.js的加载优先级是.env.production最高,其次.env.local,最后.env),或者把环境变量直接写入pm2的env配置里。

我把API密钥、数据库连接串这类敏感信息放在pm2的ecosystem.config.js的env节点里,这样代码仓库里不会泄露,服务器上也有统一管理的位置。

6.2 构建时Node内存不足报错

npm run build中途报错,提示JavaScript heap out of memory。这个和运行时OOM是两个场景,构建时的内存不够通常发生在页面数量多、依赖复杂时。

解决办法是在构建命令前加内存参数:

NODE_OPTIONS="--max-old-space-size=2048" npm run build

或者直接在package.json里配置好:

{ "scripts": { "build": "NODE_OPTIONS=\"--max-old-space-size=2048\" next build" } }

6.3 pm2中日志文件无权限写入

pm2默认以你当前用户启动进程,日志目录如果创建在/var/log/下,非root用户可能没有写入权限。表现为pm2 logs显示没内容,但进程又确实在跑。

解决办法:把日志目录改成用户有写权限的路径,比如/home/ubuntu/logs/my-next-app/,或者设置目录权限时加上chmod -R 755。我自己习惯放在应用目录下的logs/文件夹,配合logrotate做轮转,省去权限烦恼。

6.4 反向代理背后获取客户端真实IP

前面提到X-Forwarded-For,实际使用时还发现一些更细节的情况:如果多层代理,X-Forwarded-For可能会追加多个IP。后端解析时应该取第一个非信任代理的地址,而不是简单取最后一个。有些框架会自动解析,Next.js里可以自己写一段处理逻辑,按逗号分隔后取第一个IP即可。

6.5 定时任务和cluster模式的冲突

如果应用内部用了setInterval跑定时任务,比如定时清理缓存、定时抓取数据,配合pm2的cluster模式多实例跑时,会导致同一任务被执行多次。如果必须用cluster模式,要么把定时任务独立成一个单独进程管理,要么给实例加上环境变量标记,只让特定实例执行。我的建议:能用Redis之类的分布式调度就尽量用,没有的话维护一个"单实例跑定时任务,剩余实例纯处理请求"的配置模式。

7. 部署验收清单:上线前逐项核对

部署完成不代表结束,上线之前我习惯过一遍下面的检查项,避免"看起来正常,用了才发现问题":

  • 命令行执行pm2 status,确认应用状态是online,重启次数(restarts)没有异常飙升。
  • 使用curl -I https://你的域名,确认返回200,并且响应头里能看到X-Powered-By不是默认的、Content-Type正确。
  • 检查日志:pm2 logs my-next-app --lines 100,确认没有报错堆栈,特别是数据库连接、外部API调用是否稳定。
  • 浏览器访问首页,执行查看源代码,确认HTML内容里面有实际渲染出来的数据,而不是空壳应用容器。
  • 检测一个带getServerSideProps的动态页面,刷新几次,确认每次请求都有新的服务端渲染内容。
  • 测试移动端访问效果,以及快速连续点击几个页面,观察有没有明显卡顿或报错。
  • 确认服务器重启后应用能自动恢复:sudo reboot,再等一两分钟,访问域名验证。

下面是我做的一个简单验收表格,方便你逐项对比:

检查项检查方式期望结果
进程状态pm2 statusonline,restarts为个位数
服务响应curl -IHTTP 200,HEAD完整
错误日志pm2 logs无ECONNREFUSED、EADDRINUSE
SSR动态内容浏览器查看源代码包含服务端渲染数据
开机自启重启服务器后访问无需手动启动,自动恢复
内存状态pm2 prettylist内存稳定,无明显增长趋势

最后再分享一个小技巧:给pm2配一个"内存泄漏自检"条件,比如max_memory_restart设置成应用正常运行内存峰值的1.5倍,如果频繁触发自动重启,就说明代码里大概率有内存泄漏,而这个问题如果靠手工排查会非常痛苦。我经历过一次内存泄漏的排查过程,整整花了一个周末,最后发现是缓存Map没有清理机制,数据一直堆积。所以别迷信"重启大法",pm2帮你扛住一时的故障,根本的代码质量问题还是得回到源头去解决。

这次把Next.js部署上线的过程中,最深刻的体会是:部署本身不是一句命令的事,它像是对项目的又一次整体体检。环境配置、依赖管理、进程守护、反向代理、日志监控,每个环节都和运行时的稳定性强相关。真正把这些细节都处理好了,项目才算是在服务器上"站稳"了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进