ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Nginx与Node.js服务器部署实战:从安装到反向代理配置

Nginx与Node.js服务器部署实战:从安装到反向代理配置 最近帮一台新服务器做上线前部署需求特别朴素装好nginx再把node环境搞定让一个api服务跑起来对外提供服务。原以为就是两条命令的事真动手才发现光是nginx的安装方式、node的版本选择、反向代理的路径规则就能埋下好几个坑。这篇文章不打算重复官方文档而是把我这次完整的部署过程、选型逻辑和踩坑记录整理出来给同样需要在服务器上搞定nginx与node的朋友一条可以直接照着走的路。如果你是第一次接触这两个东西也能跟着从上到下把环境搭好如果你已经装过重点看第三节的版本管理和第五节的故障排查那里有平时文档里不一定会写的细节。1. Nginx Node.js是什么神仙组合先想清楚为什么需要两层服务1.1 Node.js明明能直接跑HTTP服务为什么非要前面加一层NginxNode.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 生产环境的典型拓扑与职责划分标准的架构是浏览器 → nginx80/443 → Node应用127.0.0.1:3000。Node应用在这里监听内网地址或本机地址不直接暴露公网端口所有进入的流量都走nginx这一层。这样做最大的好处是安全。Node进程不用以root身份去监听80端口也就少了一个攻击面同时nginx负责TLS终止证书放在nginx这一层就够了Node内部保持HTTP明文通信减少了应用代码处理证书的复杂度。性能上nginx处理静态文件、连接复用、HTTP/2、负载均衡这些事比Node自己做得更熟练把合适的活交给合适的工具这是NginxNode架构能扛住高并发的主要底气。还有一个容易被忽略的点链路里加了一层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 -yCentOS/RHEL平台sudo yum install epel-release -y sudo yum install nginx -y用包管理器装完配置文件统一放在/etc/nginx下站点配置在/etc/nginx/conf.d或sites-enabledUbuntunginx自带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 离线环境装Nginxaarch64纯内网实操离线内网环境是实际项目中很常见的情况尤其是一些内网部署机器是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构建工具对版本有硬性要求新项目无历史包袱当前偶数LTS20/22/24稳定且维护期长需要跑commitlint等新工具18工具本身有engines限制顺便说一句网上关于node 24.19如何配置commitlint的问题我碰到过一两次。如果你是刚初始化项目直接npx commitlint --init它会按当前node版本装合适的包如果报engines不满足多半是node升到了项目锁定版本之外要么nvm切换回锁定版本要么升级commitlint相关依赖。切忌硬改node_modules。3.2 nvm安装与全局配置最推荐的路径nvmNode 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 20nvm还有个值得说的功能可以给某个项目固定版本在项目目录写一个.nvmrc文件内容只要写20就行配合nvm use命令可以快速切到项目所需的版本。我习惯在每个服务端项目根目录都放这个文件换机器、换人都不容易踩版本坑。国内网络环境下nvm install可能比较慢可以设置镜像变量export NVM_NODEJS_ORG_MIRRORhttps://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-components1 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.xzx64还是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 weight2; server 127.0.0.1:3001 weight1; 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拿到有效响应。排查链路我建议按下面几步走顺序不要乱确认Node进程是否活着ps -ef | grep node。如果根本没这个进程那就是Node没起来或崩了看服务日志pm2 log或nohup输出。确认Node监听的端口是否正确ss -lntp | grep 3000。如果显示服务监听在127.0.0.1:3000但nginx配置里写成了127.0.0.1:3001端口对不上自然502。在本机绕过nginx直接访问后端curl http://127.0.0.1:3000/health。这一步能区分问题出在应用层还是nginx转发层。看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的处理。检查防火墙。如果Node端口并没有监听在127.0.0.1而是监听在0.0.0.0且这是跨机器部署注意firewalld/iptables有没有放行对应端口。这套链路我每次排查502都用先确认应用可用再看nginx最后看系统层基本十分钟内能定位。5.2 80端口被占用的处理nginx: [emerg] bind() failednginx启动时报bind() to 0.0.0.0:80 failed (98: Address already in use)是端口被其他进程占用。用以下命令揪出占用者sudo lsof -i:80 sudo ss -lntp | grep :80CentOS上常见的是httpd占用了80Ubuntu上可能是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下面普通用户目录默认权限是700nginx用户根本进不去访问就是403。解决方式有几种把静态资源放到/var/www或/data这类公共目录或者给资源目录加ox、or权限或者直接在nginx.conf里改user指令指向能访问该目录的用户。我一般倾向于第一种用权限去迁就nginx用户而不是改nginx运行身份安全边界更清晰。再看SELinuxCentOS/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的问题还是后端的问题等确认后端一切正常后再打开浏览器访问域名你会少走很多弯路。
返回列表