☰
Nginx与Node.js服务器部署实战:从安装到反向代理配置
2026/9/29 5:02:25 网站建设 项目流程

最近帮一台新服务器做上线前部署,需求特别朴素:装好nginx,再把node环境搞定,让一个api服务跑起来对外提供服务。原以为就是"两条命令的事",真动手才发现,光是nginx的安装方式、node的版本选择、反向代理的路径规则,就能埋下好几个坑。

这篇文章不打算重复官方文档,而是把我这次完整的部署过程、选型逻辑和踩坑记录整理出来,给同样需要在服务器上搞定nginx与node的朋友一条可以直接照着走的路。如果你是第一次接触这两个东西,也能跟着从上到下把环境搭好;如果你已经装过,重点看第三节的版本管理和第五节的故障排查,那里有平时文档里不一定会写的细节。

1. Nginx + Node.js是什么神仙组合:先想清楚为什么需要两层服务

1.1 Node.js明明能直接跑HTTP服务,为什么非要前面加一层Nginx

Node.js自带http模块,一个app.listen(3000)就能对外提供服务,很多人第一次接触时都会产生一个疑问:既然Node自己就能当服务器,nginx不是多此一举吗?

我在很长一段时间里也觉得nginx可有可无,直到第一次把一个Node服务直接暴露到公网。几个问题马上出来了:用户量大起来,Node进程对静态资源的处理能力明显不如nginx,一张图片、一个JS文件都要挤占Node的事件循环;Node进程一旦崩了或者正在重启,所有请求直接超时;如果你有几个服务想共用一个80/443端口(比如博客和API),光靠Node自己做端口分配反而麻烦。nginx站在Node前面,承担的职责很纯粹:接住外部的HTTP流量,把请求转发给后端的Node进程,再把响应返回给客户端。这个角色在运维里叫反向代理。

nginx就像一个公司前台:来访者只需要走到前台,不需要知道具体业务部门在哪层楼。你完全不用关心后端到底是Node还是Java的Tomcat,也不关心它监听在3000还是8080,只要前台能把请求送到正确的人手里就行。这也是为什么网上会有人问"nginx支持jsp吗",严格说nginx不解析任何后端语言,它只负责转发,真正的解析还在Tomcat那边。理清这一层,后面配置就顺了。

1.2 生产环境的典型拓扑与职责划分

标准的架构是:浏览器 → nginx(80/443) → Node应用(127.0.0.1:3000)。Node应用在这里监听内网地址或本机地址,不直接暴露公网端口,所有进入的流量都走nginx这一层。

这样做最大的好处是安全。Node进程不用以root身份去监听80端口,也就少了一个攻击面;同时nginx负责TLS终止,证书放在nginx这一层就够了,Node内部保持HTTP明文通信,减少了应用代码处理证书的复杂度。性能上,nginx处理静态文件、连接复用、HTTP/2、负载均衡这些事比Node自己做得更熟练,把合适的活交给合适的工具,这是Nginx+Node架构能扛住高并发的主要底气。

还有一个容易被忽略的点:链路里加了一层nginx之后,Node服务的发布就灵活了。你可以在nginx配一个upstream,指向两台机器的Node实例,做负载均衡;也可以直接改nginx的配置把流量切到新版本,然后平滑重启Node进程,实现蓝绿发布。这些能力是单个Node进程很难靠自己完成的。

1.3 什么时候可以不用nginx

不是所有场景都适合硬套nginx。如果你只是在本地调试,或者写一个内部工具、无高并发要求,直接node server.js访问3000端口完全没问题,加一层nginx反而增加配置成本。

结合我的经验,以下三种情况可以暂时不用nginx:一是纯内网的小工具,没有公网暴露需求;二是只有一个低频API服务、没有静态资源和多应用共存的;三是你还在开发阶段,频繁改代码、重启进程,用nginx反而要跟着改配置。生产上线再补上不迟。

2. 装Nginx:从包管理器到离线编译,按场景选路线

2.1 apt/yum安装:十分钟搞定但版本可能旧

如果服务器能正常访问外部软件源,最省事的方式就是用系统包管理器。Ubuntu/Debian平台:

sudo apt update sudo apt install nginx -y

CentOS/RHEL平台:

sudo yum install epel-release -y sudo yum install nginx -y

用包管理器装完,配置文件统一放在/etc/nginx下,站点配置在/etc/nginx/conf.d或sites-enabled(Ubuntu),nginx自带systemd服务脚本,所以管理起来很清爽:

sudo systemctl enable nginx --now systemctl status nginx

这个方案的缺点是版本通常落后于官方,而且你没法自定义编译参数。比如你想用nginx官方仓库里才有的新特性,或者想给nginx加第三方模块,就得考虑编译安装。如果只是为了跑一个反向代理,系统源里的版本完全够用,我生产环境至少有一半的nginx是apt/yum装的,稳定压倒一切。

2.2 编译安装:模块控制力和版本自由度

需要新版本或特定模块时,编译安装是更灵活的路线。官方源码包可以在nginx.org下载,基本流程是:

wget https://nginx.org/download/nginx-1.26.2.tar.gz tar -xzf nginx-1.26.2.tar.gz cd nginx-1.26.2 ./configure --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_stub_status_module \ --with-http_gzip_static_module make -j$(nproc) sudo make install

编译前先确认gcc、make、pcre-devel、zlib-devel、openssl-devel这几个依赖都在,缺哪个configure就会报哪个。把--prefix指定为/usr/local/nginx后,所有文件都装在这个目录里,后续升级、卸载都很可控,不会污染系统其他目录。

这里我想补充一个教训:编译安装的nginx没有systemd服务脚本,很多人装完直接/usr/local/nginx/sbin/nginx启动,进程是起来了,但开机自启没有。建议自己写一个systemd unit文件,或者至少把启动命令加到rc.local里。我写过很多次,每次都要提醒自己别漏。

2.3 离线环境装Nginx:aarch64纯内网实操

离线内网环境是实际项目中很常见的情况,尤其是一些内网部署,机器是aarch64架构,完全不能访问外网。这时候有两条路线:

第一条:在一台能联网的同架构机器上下载好rpm/deb包,rpm方式可以用yum install --downloadonly --resolve来拉取nginx及其依赖,然后拷到内网,用yum localinstall nginx-*.rpm安装。aarch64一定要认准包名里的aarch64字样,x86_64的包装不进去,这是一个很容易犯的低级错误。

第二条:如果连依赖包都拉不齐,或者需要定制模块,就直接在能联网的机器上编译好,把整个/usr/local/nginx目录打包拷进内网,只要glibc版本兼容(可以先ldd /usr/local/nginx/sbin/nginx检查动态库依赖),解压后就能跑。这个方法更笨但更稳,我一个内网项目就是用这种方式把编译好的nginx目录整体拷过去,几分钟就把服务拉起来了。

2.4 Nginx平滑升级的正确姿势

在线环境里nginx升级也有一些讲究。直接kill掉nginx进程再替换二进制会闪断连接,生产环境不能这么干。nginx官方支持平滑升级:编译好新版本后,不要make install,先把新的objs/nginx二进制拷到nginx sbin目录,然后向旧master进程发送USR2信号:

sudo cp objs/nginx /usr/local/nginx/sbin/nginx sudo kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid)

此时旧master进程会把它管理的worker进程全部交给新master,旧进程改名为nginx.oldbin,等待已有连接处理完后再自然退出。如果新版本有问题,也可以立刻切回。平滑升级的完整流程网上有很多,但我建议你在测试环境先演练一遍,别在线上边查手册边操作。

3. 装Node.js:版本管理是所有坑的根源

3.1 先花两分钟确定Node版本再动手

Node.js的安装比nginx更容易,真正的坑不在安装本身,而在版本选择。很多人打开官网就点"最新版",结果项目一跑就报node-sass或native module编译错误。

版本怎么定?先看项目里有没有package-lock.json或yarn.lock,锁文件里一般能看到项目实际使用的node版本范围;再看package.json的engines字段:

"engines": { "node": ">=18 <21" }

如果没有这些信息,就参考项目的构建工具。比如Angular 9的项目老老实实用Node 12或14,硬上Node 24大概率直接编译失败;commitlint这类工具则要求Node版本符合一定范围,版本太低反而装不上。生产环境我的经验是优先选偶数LTS版本,比如20/22/24,奇数版本(如23/25)尝鲜可以,上线不建议。

我把常见的版本选择思路整理成下面这个表,方便对照着判断:

场景建议Node版本原因
老项目,用node-sass/webpack414或16老依赖编译兼容性差
Angular 9项目12或14构建工具对版本有硬性要求
新项目,无历史包袱当前偶数LTS(20/22/24)稳定且维护期长
需要跑commitlint等新工具>=18工具本身有engines限制

顺便说一句,网上关于"node 24.19如何配置commitlint"的问题,我碰到过一两次。如果你是刚初始化项目,直接npx commitlint --init它会按当前node版本装合适的包;如果报engines不满足,多半是node升到了项目锁定版本之外,要么nvm切换回锁定版本,要么升级commitlint相关依赖。切忌硬改node_modules。

3.2 nvm安装与全局配置:最推荐的路径

nvm(Node Version Manager)是Linux上管理Node版本最舒服的工具。它允许你同时装多个Node版本,随时切换,还能为每个目录指定默认版本。安装方式是一段脚本:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

脚本执行完会往~/.bashrc里写几行配置,新开终端后就能用nvm命令了。如果source ~/.bashrc后还提示command not found,多半是NVM_DIR没有正确写入,检查一下这行:

export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

然后安装并设置默认版本:

nvm install 20 nvm alias default 20 nvm use 20

nvm还有个值得说的功能:可以给某个项目固定版本,在项目目录写一个.nvmrc文件,内容只要写20就行,配合nvm use命令可以快速切到项目所需的版本。我习惯在每个服务端项目根目录都放这个文件,换机器、换人都不容易踩版本坑。

国内网络环境下nvm install可能比较慢,可以设置镜像变量:export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node,然后再执行nvm install,下载速度会明显快很多。

3.3 二进制包方式:免编译、目录干净、适合服务器

如果你不想在服务器上装nvm(或者觉得nvm引入一层额外的东西不够"素"),可以直接下载Node官方提供的二进制tar包。注意不是源码包,是linux-x64或linux-arm64结尾的那个tar.xz。

以Node 20 LTS为例:

wget https://npmmirror.com/mirrors/node/v20.12.2/node-v20.12.2-linux-x64.tar.xz mkdir -p /opt/node tar -xf node-v20.12.2-linux-x64.tar.xz -C /opt/node --strip-components=1 ln -s /opt/node/bin/node /usr/local/bin/node ln -s /opt/node/bin/npm /usr/local/bin/npm

解压到/opt/node后,node和npm就在/opt/node/bin下面,软链到/usr/local/bin是为了让普通用户直接能执行。这种方式的优势很明显:不编译、不依赖系统环境、目录干净,删除时把/opt/node和软链删掉就完全卸载。

有个细节要提醒:npm在Linux上默认是shell脚本,软链没问题;但Windows下的npm是个.ps1文件,PowerShell执行时会遇到禁止运行脚本的问题,所以二进制包安装路径在Windows上一般是手动加PATH而不是软链。这个问题我在Windows本地开发时经常遇到,后面故障排查里再细说。

3.4 离线安装Node与npm国内镜像加速

Node的二进制包天然适合离线安装:在联网机器上下载对应架构的tar.xz(x64还是arm64要分清),拷到内网后直接解压、软链,就完成了。不需要编译,比nginx离线安装简单得多。

离线机器装好node后,npm默认registry是官方源,如果内网不能访问外网,npm install会一直卡着不动。解决办法是配镜像源,如果内网有私有npm仓库就配私有仓库地址;如果能出网但慢,就配国内镜像:

npm config set registry https://registry.npmmirror.com

配置完可以用npm config get registry确认。对拉取历史版本的情况,npmmirror的node镜像里也有完整的版本列表,node历史版本国产镜像安装包下载这个需求基本都能满足。我还建议把npm config set fund false和npm config set audit false关掉,节省每次安装的时间,纯粹的个人偏好,但实测有效。

4. 把Node应用挂到Nginx下的配置实战

4.1 最小反向代理配置:注意proxy_pass有没有斜杠的区别

假设Node应用监听127.0.0.1:3000,下面是nginx里一个最小但完整的server块:

server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

把这段配置放到/etc/nginx/conf.d/app.conf里,nginx -t测试通过后systemctl reload nginx,一个Node应用就被nginx代理起来了。

这里有一个极其容易踩的坑:proxy_pass后面的URI有没有斜杠,转发规则完全不同。没有斜杠时,nginx会把完整请求URI原样转发给后端,比如访问/api/user,后端收到就是/api/user;当你在proxy_pass末尾加了斜杠,比如proxy_pass http://127.0.0.1:3000/,访问/api/user时nginx会用斜杠后面的路径替换掉location匹配的前缀,后端收到的实际是/user。这个差异经常导致接口404,排查起来又隐蔽,我建议在配置里统一不加斜杠,除非你有明确需要重写路径。

4.2 upstream负载均衡与多实例部署

单个Node进程扛高并发时,最简单有效的手段是开多个进程。可以用pm2的cluster模式,也可以手动起多个实例监听不同端口,然后让nginx在它们之间分发请求:

upstream node_backend { server 127.0.0.1:3000 weight=2; server 127.0.0.1:3001 weight=1; keepalive 32; } server { listen 80; server_name example.com; location / { proxy_pass http://node_backend; proxy_http_version 1.1; proxy_set_header Connection ""; } }

upstream块里weight表示权重,流量会按权重比例分发;keepalive 32让nginx与后端保持一批长连接,避免每个请求都重新建连。这里记得要设置proxy_http_version 1.1并清空Connection请求头,否则keepalive不生效。

Node进程本身可以不做cluster,由nginx在网络层做负载均衡,进程各自独立,一个进程崩溃不会把整个服务拖垮;nginx会把请求自动转到还活着的进程上,这就是很朴素的高可用。配置完可以用ab -n 10000 -c 100 http://127.0.0.1/压一下,观察后端两个端口的连接数分布是否接近权重比例。

4.3 静态资源让Nginx直接返回,Nginx负责缓存

Node应用一般都有静态资源:前端打包后的JS、CSS、图片。把这些资源直接交给nginx处理,Node只处理API,是性能提升最明显的一步。

假设前端构建产物在/data/www/dist目录,API在Node的3000端口,可以这样配:

server { listen 80; server_name example.com; root /data/www/dist; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; } location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 7d; add_header Cache-Control "public, immutable"; try_files $uri =404; } }

try_files会让所有非静态路径都回退到index.html,这是单页应用常见的做法。而带静态资源后缀的请求,直接用root目录找文件并设置7天强缓存,根本不会打进Node。要注意root的语义是"把URI拼到root后面找文件",所以文件实际路径是/data/www/dist+ URI。如果碰到文件在别的目录、想换路径前缀的情况,需要用alias而不是root,两者差别新手很容易搞混。

4.4 WebSocket长连接的代理配置

Node的实时服务经常走WebSocket(比如socket.io,虽然不是纯粹的ws协议)。nginx默认情况下不支持升级连接,需要显式设置:

location /ws/ { 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_read_timeout 3600s; }

proxy_set_header Upgrade和Connection这两行把HTTP Upgrade请求头传给后端,nginx才能识别并隧穿WebSocket流量。连接建立后是长连接,默认的proxy_read_timeout只有60秒,空闲超过60秒nginx会主动断开,所以实时应用一般要把这个值调大,或者按业务心跳机制来设定。我踩过一次:WebSocket白天好端端的,过个午休回来就掉线,查到最后就是proxy_read_timeout的锅。

4.5 主域名、二级域名与多应用共存

一台服务器上同时跑多个Node应用时,用域名区分是最清晰的思路。比如主站跑3000端口,API站跑4000端口,nginx两个server块各配各的server_name即可:

server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:3000; } } server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:4000; } }

如果想让所有二级域名都指到同一个后端,可以写server_name *.example.com。这里要注意DNS那边先把A记录解析到位,不然nginx配了也没用。如果还想用同一个域名按路径区分不同的应用,比如/blog和/api分别走不同端口,location挂不同前缀就行,但我会更推荐按域名拆分,配置清晰,也方便以后把blog迁到独立服务器。

涉及HTTPS时,可以用certbot自动申请证书,也可以从证书商下载nginx类型的证书(.crt/.pem和.key),在server块里配置ssl_certificate和ssl_certificate_key。证书商一般会提供nginx专用格式,下载时选nginx类型就行。如果开了客户端证书校验又提示no required ssl certificate was sent,那就是客户端没带着证书访问,属于常见排查点。

5. 装完跑不起来的典型故障,按排查链路一个个来

5.1 502 Bad Gateway的完整排查思路

装完nginx和node,访问域名时浏览器显示502,这是最经典的问题。502表示nginx已经收到请求,但没能从后端Node拿到有效响应。排查链路我建议按下面几步走,顺序不要乱:

  1. 确认Node进程是否活着:ps -ef | grep node。如果根本没这个进程,那就是Node没起来或崩了,看服务日志(pm2 log或nohup输出)。
  2. 确认Node监听的端口是否正确:ss -lntp | grep 3000。如果显示服务监听在127.0.0.1:3000,但nginx配置里写成了127.0.0.1:3001,端口对不上,自然502。
  3. 在本机绕过nginx直接访问后端:curl http://127.0.0.1:3000/health。这一步能区分问题出在应用层还是nginx转发层。
  4. 看nginx错误日志:tail -f /var/log/nginx/error.log。常见的报错形如connect() to 127.0.0.1:3000 failed (111: Connection refused),说明后端确实没监听;如果是(13: Permission denied),则是SELinux在拦截,转到5.3的处理。
  5. 检查防火墙。如果Node端口并没有监听在127.0.0.1而是监听在0.0.0.0,且这是跨机器部署,注意firewalld/iptables有没有放行对应端口。

这套链路我每次排查502都用,先确认应用可用,再看nginx,最后看系统层,基本十分钟内能定位。

5.2 80端口被占用的处理:nginx: [emerg] bind() failed

nginx启动时报bind() to 0.0.0.0:80 failed (98: Address already in use),是端口被其他进程占用。用以下命令揪出占用者:

sudo lsof -i:80 sudo ss -lntp | grep ':80'

CentOS上常见的是httpd占用了80,Ubuntu上可能是apache2或snap的一些服务。处理方式有两种:如果那个服务不需要了,直接停掉再启动nginx;如果它必须保留,就调整nginx监听其他端口,比如先跑在8080验证。不要一上来就kill -9,先确认进程身份,误杀系统服务会造成更大的故障。

补充一个Windows场景:nginx在Windows上只是解压zip后直接运行nginx.exe,没有systemd服务脚本,也不能自己开机自启。很多人在Windows上装nginx只是为了本地调试,这个场景里同样可能遇到80端口被占用,最常见的是IIS或上一个nginx实例还开着,可以用netstat -ano | findstr :80查到PID再决定处理。Windows上的nginx不推荐用于生产,但本地开发够用。

5.3 权限与SELinux拦路问题

权限问题和SELinux是Linux部署里最容易被忽视的两个坑,因为现象都是功能不正常,但日志不直接说"权限不足"。

先看权限:nginx的worker进程默认以nginx用户(或编译时指定的用户)运行,如果你把静态文件放在/home/xxxx/project/dist下面,普通用户目录默认权限是700,nginx用户根本进不去,访问就是403。解决方式有几种:把静态资源放到/var/www或/data这类公共目录,或者给资源目录加o+x、o+r权限,或者直接在nginx.conf里改user指令指向能访问该目录的用户。我一般倾向于第一种,用权限去迁就nginx用户,而不是改nginx运行身份,安全边界更清晰。

再看SELinux:CentOS/RHEL默认开启SELinux,即使防火墙放行了、端口也没问题,nginx反向代理连接到本地/远程端口时仍可能被SELinux拒绝,error.log里会出现connect() to 127.0.0.1:3000 failed (13: Permission denied)。处理方式是打开httpd_can_network_connect这个布尔值:

sudo setsebool -P httpd_can_network_connect 1

-P参数表示持久化,重启不失效。这个坑我遇到不止一次,每次都在想"端口明明通、进程明明活着、为什么还是502",最后才发现是SELinux在拦。如果你用的是Ubuntu,默认没有SELinux,可以直接跳过这一段。

5.4 Node版本与项目依赖不匹配

很多项目跑不起来不是nginx的问题,而是Node版本和项目依赖不匹配。典型的报错有 node-sass 编译失败、OpenSSL 3.x 的 md4 相关报错、某些native模块安装时找不到预编译包而现场编译失败。

如果项目锁定的Node版本是16或18,你非要装个Node 24去跑,工程依赖里如果有老版本node-sass、老版本webpack,大概率起不来。处理方式就两条:一是用nvm切换到项目支持的版本(这也是我坚持推荐nvm的原因);二是升级依赖,把node-sass换成sass或dart-sass,把老webpack版本往上升。哪种方式更合理要看项目维护状态,长期维护的项目建议升级,短期的老项目直接用nvm锁版本最快。

判断项目的Node版本有一个取巧的办法:看package-lock.json里lockfileVersion字段对应的npm版本,再看npm版本对应的Node支持范围。虽然不精确,但能帮你快速收敛方向。遇到 24.19 这种新版本配合commitlint时,报错大多是commitlint某个包要求的最低Node版本比项目当前高,升级对应包而不是降Node版本通常更省事。

5.5 Windows本地开发相似坑:npm.ps1禁止运行脚本

虽然主线是Linux服务器部署,但很多人的开发机是Windows。装上Node后,在PowerShell里敲npm,可能会遇到:

npm : 无法加载文件 D:\Program Files (x86)\node\npm.ps1,因为在此系统上禁止运行脚本。

这是PowerShell的执行策略限制,不是Node本身的问题。用管理员身份打开PowerShell执行:

Set-ExecutionPolicy RemoteSigned

然后重新打开终端,npm就能正常使用了。如果你不想动全局执行策略,也可以临时加-ExecutionPolicy Bypass来启动当前进程。这个坑几乎每个Windows上的Node新手都会遇到,看到了别以为自己的Node装坏了。

我这次完整的部署过程里最有感触的一点是:nginx和Node单独装都简单,难的是把它们组合进一个稳定可维护的体系里。版本选型、路径规则、SELinux、长连接超时,每一样都值得花十分钟想清楚,否则小毛病会反复回来找你。如果你照着这篇文章装完还有问题,我的建议是先去翻/var/log/nginx/error.log,那行报错往往比任何教程都诚实。最后再分享一个小技巧:调试反向代理时,用curl -v http://127.0.0.1 -H "Host: example.com"直接看响应头和状态码,能快速判断是nginx的问题还是后端的问题;等确认后端一切正常后,再打开浏览器访问域名,你会少走很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询