ARTICLE DETAIL

资讯详情

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

Nginx部署实践:安装配置、反向代理、SSL证书与安全加固全解析

Nginx部署实践:安装配置、反向代理、SSL证书与安全加固全解析 如果你负责部署过Doris、FastDFS、Zabbix这类系统大概会有同样的感受不管业务后端是什么第一件事永远是nginx安装部署。它既是最常用的静态文件服务器又是反向代理入口还是各种内部服务的统一流量网关。很多项目所谓部署本质上就是先立起一台nginx再把业务服务接到它后面。这篇内容我想聚焦两类问题一类是刚接触服务器的人想知道nginx到底怎么装、装完怎么配另一类是已经能跑通基础环境的人想弄清楚为什么证书替换了不生效、为什么浏览器报跨域、为什么代理连接数上不去。我会把安装方式的取舍、多站点与反向代理配置、和其他服务协作时的高频坑以及上线前必须做的加固与监控都讲一遍都是我在实际服务器和容器环境里操作过的经验。1. 先把nginx装对包管理器、源码编译与Docker三选一1.1 包管理器安装适合大多数常规业务的第一选择如果你的Linux发行版是CentOS、Ubuntu这类主流系统并且没有特殊模块需求第一选择就是包管理器安装。CentOS上执行yum install epel-release -y yum install nginx -yUbuntu上更简单apt update apt install nginx -y这里有一个容易忽略的点CentOS默认官方仓库里其实没有nginx包必须先把epel-release装好否则yum会提示找不到包很多新手卡在这一步。装完之后用nginx -v检查版本再执行systemctl enable --now nginx设置开机自启并立刻启动。包管理器安装最大的优势是生态集成好自动创建nginx用户、自动生成/var/log/nginx和/etc/nginx目录、自带systemd服务脚本卸载的时候也比较干净。缺点是版本滞后比如Ubuntu 20.04仓库里的nginx还是1.18HTTP/3、某些新指令都用不上。但如果你的业务要兼容老版本的PHP-FPM或旧编译模块仓库里这个旧版本反而是优点。对于纯静态资源托管、简单反向代理、日志切割这些常规需求yum/apt装出来的nginx完全够用。我帮人排查过不少nginx装不上的问题八成是安装源没配对还有两成是80端口被Apache或其他服务占了改一下listen端口就行。1.2 源码编译安装需要新版本和自定义模块时的必经之路当你要用官方仓库里没有的模块比如TCP流转发、HTTP/2、nginx-sticky-module这类第三方扩展就必须走源码编译。编译前先确认依赖yum install -y gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel然后下载源码包configure指定安装目录和需要的模块wget https://nginx.org/download/nginx-1.26.2.tar.gz tar zxvf nginx-1.26.2.tar.gz cd nginx-1.26.2 ./configure --prefix/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_stub_status_module \ --with-stream \ --with-stream_ssl_module \ --with-http_realip_module \ --with-http_gzip_static_module make -j4 make install为什么这些参数值得加--with-http_stub_status_module是监控nginx必须要用的状态页--with-stream是做四层TCP/UDP代理时才有的功能比如你要用nginx代理MySQL或Redis流量--with-http_realip_module在nginx前面还有负载均衡设备时用来取真实客户端IP不装的话日志里全是内网地址。源码编译最大的坑是第一次configure时漏了某个模块后面想补不能像装插件一样加一个.so因为nginx默认把所有模块静态编译进二进制。你必须重新执行configure、make、make install所以第一次就把常用参数都带上省得返工。编译完成后要手动加一个软链否则每次都要写全路径ln -s /usr/local/nginx/sbin/nginx /usr/local/bin/nginx我习惯顺手写一个systemd服务文件这样可以用systemctl restart nginx而不是手动去敲/usr/local/nginx/sbin/nginx -s reload多台服务器管理时会顺手很多。1.3 Docker与容器化最干净但最需要理解镜像行为Docker方式在开发环境、测试环境甚至是生产环境普及率都非常高。启动一个最基础的nginx容器docker run -d --name nginx -p 80:80 \ -v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \ nginx:stable很多人这么干之后立刻遇到问题容器启动失败或者启动成功但访问404。常见原因之一是官方镜像里自带的/etc/nginx/conf.d/default.conf占用了80端口并监听server_name localhost你把整个conf.d目录挂载覆盖掉之后default.conf没了但如果你自己的配置也监听80且server_name和默认规则冲突就可能出现行为诡异的情况。搜索引擎里有个很典型的词nginx alpine挂载conf.d报错。我在alpine镜像上遇到过的报错多半是权限问题。alpine镜像里worker进程以nginx用户运行而挂载进去的配置文件如果权限是700或者属主是宿主机root容器内读取会直接Permission denied。处理方法很简单给目录和文件至少755/644权限还不行就检查SELinux是否拦截挂载。先用临时关闭SELinux验证是快速定位的好办法但生产环境要回到正确的上下文配置而不是永久关掉。Windows上想用Docker跑nginx就是Docker Desktop加WSL2方案最常见的坑是IIS或Hyper-V把80端口占了。先用netstat -ano | findstr :80找到占用进程要么停掉服务要么把端口映射改成8080。开发机不一定要用80端口后面反正都要走nginx转发。docker-compose方式把端口、挂载、环境变量写在一起团队协作更省心services: nginx: image: nginx:stable ports: - 80:80 - 443:443 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro restart: unless-stopped如果你要管理多套环境容器化nginx基本是不二之选配置能随代码仓库走。但也要理解容器只是封装nginx配置逻辑和报错排查方法跟物理机没有本质区别。想找容器里的错误日志照样是docker logs nginx和docker exec -it nginx nginx -t这两招。三种安装方式没有绝对好坏按场景选就行。我也见过直接把三种方式混着用的团队开发环境用Docker预发环境用包管理器核心生产环境用源码编译。只要最终配置管理方式统一问题就不大。安装方式优点缺点适合场景包管理器快速、systemd集成好、卸载干净版本滞后常规静态与反向代理源码编译版本新、可自定义模块编译耗时、升级麻烦特殊模块、高性能场景Docker环境隔离、配置可版本化需要理解镜像和权限容器化、多环境开发2. 安装不是终点把这些配好才算真部署多站点、反向代理与HTTPS2.1 多站点与自定义域名server_name与listen的配合本地虚拟机、多端口nginx、开发环境多站点自定义域名配置这类需求我见了太多次。你要在开发机上用nginx跑几个前端项目希望访问project1.test时打开项目一访问project2.test时打开项目二还想用不同端口区分内部服务。操作分两步。第一步改本机hosts把自定义域名指向nginx所在机器。如果Windows宿主机要访问虚拟机里的nginx就改C:\Windows\System32\drivers\etc\hosts192.168.56.101 project1.test 192.168.56.101 project2.test第二步在nginx里为每个站点建一个server块。我推荐在/etc/nginx/conf.d/下每个项目放一个conf文件比如project1.confserver { listen 80; server_name project1.test; root /srv/project1; index index.html; }另一个项目想用不同端口时server { listen 8081; server_name project2.test; root /srv/project2; }有人喜欢把所有server块全塞进一个nginx.conf里短时间没问题但配置一多维护就是灾难。nginx默认通过/etc/nginx/nginx.conf里的include /etc/nginx/conf.d/*.conf;加载所有conf文件这个机制要善用。改完配置后执行nginx -t验证再nginx -s reload。如果两个server块监听同一端口nginx会根据请求头里的Host字段匹配server_name匹配优先级是精确匹配大于通配符、通配符大于正则、最后才落到default_server。实际项目里有个坑我踩过两个conf文件里写了完全相同的server_name结果所有请求都进了先加载的那个server。nginx有时候不会因为重名直接报错只是按加载顺序默默处理检查配置时很难发现。2.2 反向代理一条proxy_pass但是有讲究nginx反向代理配置看起来就是一小段location但细节非常多。基础写法location /api/ { proxy_pass http://127.0.0.1:8080/; 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_pass http://127.0.0.1:8080/;末尾有没有斜杠路径规则完全不同。假设请求是/api/userproxy_pass不带斜杠时nginx会把原始路径原样转发给后端带斜杠时nginx会把匹配到的/api/前缀去掉再拼接后端收到的就是/user。很多人觉得写错了也差不多实际上后端接口如果按/api/user注册的你用带斜杠的配置去代理百分之百404。第二为什么要设置Host头后端如果是SpringBoot这类框架很多接口会校验请求头里的Host和X-Forwarded-Proto。如果不转发Host后端拿到的可能是内网IP或者nginx容器的ID做重定向跳转时会跳到错误的地址。upstream做负载均衡是这样用的upstream backend { server 10.0.0.11:8080 weight3; server 10.0.0.12:8080 weight2; keepalive 32; } server { listen 80; location / { proxy_pass http://backend; } }weight是轮询权重keepalive是每个worker和后端之间缓存的空闲连接数。这里有一个常用但容易误判的点upstream里某个server挂掉之后nginx会把请求转发到其他server但如果你没有任何健康检查机制后端恢复时nginx不会立刻感知可能继续把流量打到还在启动中的节点。生产环境建议给后端配独立健康检查不能只靠nginx这种转发失败再跳到下一个的被动机制。部署Zabbix这类系统时最常见的做法就是让nginx挂在前面把Zabbix前端和API代理到本机端口外部只开放80和443安全和维护都会轻松很多。RabbitMQ的管理界面、Kubernetes里的ingress-nginx本质上也是同一个思路nginx作为统一流量入口后面再接各种业务服务。2.3 HTTPS与SSL证书替换证书不生效的完整排查链路HTTPS配置本身不复杂server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location / { proxy_pass http://127.0.0.1:3000; } }真正让人头疼的是证书明明换了浏览器还是旧证书或配了证书却不生效。我处理过很多次这类问题基本按下面这个顺序排查能解决绝大多数情况先确认nginx没有报错执行nginx -t。如果证书文件权限有问题或者证书路径根本不存在这一步就会报出来。重新加载配置nginx -s reload。很多人只替换了pem文件没执行reloadworker进程还在用旧连接和旧证书。检查证书路径是不是指向软链。有些运维习惯用ln -s 20240101_fullchain.pem fullchain.pem这种软链方式切换证书如果软链目标没更新nginx加载的还是老文件。用ls -l /etc/nginx/ssl/一眼就能看出来。检查证书链是否完整。很多免费证书签下来的是fullchain.pem里面同时包含站点证书和中间证书。如果只传了站点证书没传中间证书PC端Chrome可能正常手机端或某些浏览器会提示证书不受信任。排除浏览器缓存。证书替换后浏览器可能还在用旧连接换无痕窗口试一次或者直接用curl -v https://www.example.com/看实际返回的证书内容。最常见的是第2步和第4步。尤其是第4步在证书下载页面只下了cert.pem忽略了chain.pem结果PC端没问题手机端一直报错。标准做法是把站点证书和中间证书按顺序拼成一个文件再指向它cat example.com.crt intermediate.crt fullchain.pem还要记得用openssl x509 -in fullchain.pem -noout -dates检查有效期确认你替换的确实是新证书而不是同名老文件。这个坑我见过很多人折腾两三天最后发现是文件名没错、文件内容压根没更新。3. nginx和其他服务协作时的常见坑Ollama代理、CORS与证书报错3.1 代理Ollama设置API Key反向代理透传Authorization把Ollama这种本地大模型服务用nginx代理出去是目前很常见的需求。Ollama默认监听本机的11434端口如果不经代理前端页面访问的就是http://服务器IP:11434会产生跨域问题API Key也没法统一管理。用nginx做一层反向代理会舒服很多server { listen 80; server_name ollama.example.com; location / { proxy_pass http://127.0.0.1:11434; 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_buffering off; proxy_read_timeout 300s; } }如果你要加API Key鉴权可以加一行proxy_set_header Authorization $http_authorization;nginx会把客户端的Authorization头原样透传给Ollama。像CherryStudio这类客户端在配置上游时要求设置apikey本质就是把Key放到请求头里nginx这里只需要保证头不被吞掉。这里有一个容易忽略的细节proxy_buffering off。Ollama的对话接口是流式输出如果开着缓冲用户在前端聊天时会感觉等很久然后一整段文本突然出现。我第一次调通就卡在这里加上off之后才变成流畅的逐字输出。另外还要把proxy_read_timeout调大因为大模型推理可能要几十秒甚至更久默认60秒超时会直接把连接断开。3.2 invalid cors request跨域配置的正确姿势nginx报invalid cors request或者浏览器控制台出现CORS policy错误十有八九是Access-Control-Allow-Origin没配对。最简单的配置是在server或location里加location /api/ { add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods GET, POST, OPTIONS, PUT, DELETE always; add_header Access-Control-Allow-Headers Content-Type, Authorization always; add_header Access-Control-Allow-Credentials true always; if ($request_method OPTIONS) { return 204; } proxy_pass http://backend; }这里为什么要用$http_origin而不是*因为如果后端需要带Cookie做登录态Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true不能同时出现浏览器会直接拒绝。用$http_origin把请求来源原样返回再配合map或referer模块做白名单既能满足跨域需求又不会把源头完全放开。很多人配跨域时只加了响应头忘了处理OPTIONS预检请求。浏览器在POST请求之前会先发一个OPTIONS如果nginx把这个OPTIONS直接转发给后端而后端又没有处理OPTIONS的逻辑预检就会失败。上面配置里用if判断OPTIONS直接返回204是当前最通用的做法。还要注意add_header的继承规则。一旦某个块里出现了add_header它并不会继承父级块里的add_header而是在本块重新定义。比如父location配了CORS头子location又配了另一个add_header子location的CORS头可能就丢了。排查这类问题别只盯着报错先把配置里所有add_header列出来。3.3 转发HTTPS时net::err_cert_common_name_invalid证书域名不匹配的定位这个报错在nginx场景里非常典型你用nginx把https://api.example.com转发到后端浏览器却提示证书common name不匹配。意思就是浏览器访问的域名和证书上写的域名对不上证书校验失败。排查办法很简单先看证书里到底写了哪些域名openssl x509 -in /etc/nginx/ssl/fullchain.pem -noout -text | grep Subject Alternative Name如果输出里没有你要访问的域名这个问题就没有配置上的解法只能换证书。正确做法是在签发证书时把需要访问的域名都加进SAN比如api.example.com和dev.example.com同时签进去。还有另一种常见情况nginx的server_name写的是A域名但你通过B域名访问证书SAN只包含A。这时候不是nginx配置的问题而是访问地址的问题。内部调试时可以在hosts里把B域名临时指向服务器IP但正式环境必须让用户访问证书里的域名。网上有让用户关闭浏览器证书校验的偏方我强烈不建议。本地测试偶尔能用一旦团队里有人把这套习惯带上生产就是事故。证书报错就老老实实从域名和证书本身解决不要走后门。4. 代理场景下的性能与连接数从worker参数到mirror超时4.1 TCP最大连接数worker_processes与worker_connections的关系总有人问nginx作为反向代理TCP最大连接数是多少这个问题没有固定答案它由三组参数共同决定。worker_processes auto; events { worker_connections 1024; }worker_processes是nginx启动的worker进程数通常建议设为CPU核心数写auto就行。worker_connections是每个worker能同时打开的最大连接数。理论上nginx能承载的最大并发连接数就是worker_processes × worker_connections。但如果你是反向代理一个客户端请求会占两个连接一个是客户端和nginx之间的连接一个是nginx和后端服务之间的连接。所以极限并发请求数大约要再除以2也就是worker_processes × worker_connections / 2。很多人上来就把worker_connections设成65535然后去压测发现根本达不到就是因为忘了考虑连接数和请求数之间的这个关系。光改nginx还不够Linux还有文件句柄限制。默认单进程最多1024个fd你配置里写worker_connections 65535也没用进程根本打不开那么多连接。要同时修改ulimit -n 65535以及/etc/security/limits.conf* soft nofile 65535 * hard nofile 65535再看内核网络队列参数sysctl -w net.core.somaxconn65535 sysctl -w net.ipv4.tcp_max_syn_backlog65535为什么不建议盲改到很大每个连接都会占内存一个连接在内核里大约几KB到十几KB一万个连接就是几百MB的占用。如果后端服务本身只能扛住几百个连接把nginx调到10万毫无意义水管再粗下游水厂不行也是白搭。4.2 mirror流量复制的超时设置nginx的mirror指令可以把请求复制一份发到另一个upstream用于线上流量回放或者影子测试。配置长这样location /api/ { mirror /mirror_test; mirror_request_body on; proxy_pass http://backend; } location /mirror_test { internal; proxy_pass http://test-backend:8080/api/; }这里有一个很实际的问题流量复制的目标通常是测试环境而测试环境往往比较慢如果一直没有响应复制请求就会一直占着nginx的worker连接。mirror的超时时间怎么调答案在mirror目标location的proxy参数里location /mirror_test { internal; proxy_connect_timeout 5s; proxy_read_timeout 10s; proxy_pass http://test-backend:8080/api/; }mirror请求超时或失败不会影响主请求的返回结果这是它能用来做流量回放的原因。但有一个隐患如果镜像目标一直不响应连接还是会在超时之前占用worker资源。所以我建议镜像服务必须设置合理超时不能默认60秒。我见过有人把mirror配到生产环境后测试后端把线程池打满主请求也跟着卡顿排查半天才发现nginx的连接池被镜像请求占满了。mirror不是免费功能资源占用要按线上并发量估算。4.3 部署Doris、FastDFS等后端时nginx的职责划分不少部署教程比如Doris安装部署FastDFS 5.05详细安装部署GenieACS安装部署都会在最后一步引入nginx。原因很简单这些服务要么管理页面需要统一入口要么多个节点需要负载均衡。以FastDFS为例storage节点的文件访问通常通过nginx模块fastdfs-nginx-module输出nginx在这里的作用就是HTTP文件访问入口。部署顺序一般是先装FastDFS的tracker和storage再装nginx并编译对应模块最后在storage节点的nginx配置里加一个location指向文件存储目录。以Doris为例多个FE节点用nginx做HTTP流量负载均衡前端连接8030端口后端接口走8040端口。配置就是upstream加proxy_pass套前面第2.2节那套逻辑就行。跟这些后端协作时最大的心得是nginx只负责流量分发和静态资源不要指望它去解决业务状态问题。如果一个接口在nginx代理后时好时坏先绕开nginx直接访问后端IP和端口确认后端本身是否稳定。这个排查手段很多人不用非要盯着nginx日志分析浪费大量时间。5. 装完不是终点加固、监控与上线前检查5.1 安全加固CTF加固题和现实环境都必须做的几件事网上经常能看到CTF nginx安全加固这类题其实它考的内容跟生产环境加固基本一致。按重要性列举几件必做的事。隐藏版本号server_tokens off;这能防止扫描器通过Server头确定nginx具体版本再去匹配已知漏洞。改完用curl -I验证一下Server头是否只剩nginx。关闭目录列表autoindex off;如果开了autoindex访问一个没有index.html的目录会把整个目录文件列出来等于把服务器文件结构暴露给所有人。我检查过不少现场默认确实不会开但总有历史配置或安装脚本把它打开过。限制敏感路径访问location ~* \.(sql|bak|sh|env)$ { deny all; } location ~ /\.git { deny all; }这个对老项目很有用很多人把数据库导出文件、备份压缩包直接放在web目录里然后被搜索引擎收录。CTF里这类叫源码泄露现实里叫安全事故。限制接口请求频率limit_req_zone $binary_remote_addr zoneapi_limit:10m rate10r/s; server { location /api/ { limit_req zoneapi_limit burst20 nodelay; proxy_pass http://backend; } }rate是平均速率burst是突发桶大小。推荐先设一个正常用户不会触发但脚本抓包更容易触发的值比如10r/s然后根据监控数据逐步调整。增加通用安全响应头add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always;这些不会让你的业务功能变强但能挡掉一部分低水平攻击和点击劫持。5.2 监控接入stub_status、Zabbix7与Prometheusnginx部署完成后你要能知道它当前有多少连接、每秒处理多少请求。最简单的方式是开启stub_status状态页location /nginx_status { stub_status; allow 127.0.0.1; deny all; }这个页面会输出Active connections、accepts、handled、requests等关键指标。注意它依赖--with-http_stub_status_module模块这就是我在第1.2节强调源码编译时要加这个参数的原因。用yum装的可以先检查一下nginx -V 21 | grep stub_status。Zabbix 7监控nginx是很成熟的方案。思路是让Zabbix通过HTTP方式去拉取http://127.0.0.1/nginx_status再配一个模板把返回的数字解析成监控项。只要状态页能访问模板基本开箱即用。Prometheus和Grafana的方案也不复杂在nginx上装nginx-prometheus-exporter通过stub_status抓取指标再暴露成Prometheus格式。Grafana上有现成的dashboard可以直接导入。这里有一个实际经验状态页不要对公网开放。虽然它只有数字没有太多敏感内容但会暴露nginx的负载情况和当前连接数对攻击者来说是有价值的侦察信息。所以上面配置里一定要allow 127.0.0.1; deny all;监控采集都走本机或内网地址。5.3 发布新配置前的固定动作最后整理一下我个人每次改完nginx都会做的检查动作算是一个上线前清单nginx -t nginx -s reload curl -sI http://127.0.0.1/ | head -n 5 tail -n 50 /var/log/nginx/error.log第1步确认语法第2步平滑生效第3步验证本地入口正常第4步看有没有新报错。听起来很简单但很多线上事故就是少了第1步直接reload或者改完配置后没看error log。另外建议把nginx.conf和conf.d目录纳入Git管理每次改动留一个commit出问题时能快速对比上一份能用的配置和刚改的配置差在哪。如果你管理的机器不止一台可以用Ansible这类工具统一分发配置然后批量reload。不要在一台机器上手改一下就完事多节点配置漂移是运维事故的重要来源。我在实际部署中被坑出来的一个习惯是所有改动过nginx配置的服务器至少观察十分钟error.log再关终端。很多配置问题不是reload时立刻爆出来的而是第一个真实请求进来之后才在日志里出现。你说不上这是严谨还是强迫症但大多数线上事故其实都是能提前在日志里看到的。
返回列表