Nginx多域名多证书配置实战:从架构设计到多服务分流详解
发布时间:2026/10/18 6:53:35 锦皓数字建站

1. 配置前的整体思路与架构设计不少人第一次接触多域名多证书的 Nginx 配置时第一反应就是“在一个配置文件里堆多个 server 块不就行了”。这个想法方向没错但真正落地的时候80% 的问题都出在配置文件的组织方式和细节参数上而不是 server 块本身写错了。先说清楚我们在解决什么问题一台服务器同一个 IP同一个 80/443 端口要同时挂多个网站每个网站用独立的域名访问每个域名有自己的 HTTPS 证书同时这些网站背后可能对应不同的后端服务——可能是 Java 服务、Python 服务、Node 服务也可能就是纯静态文件。所有流量都先进 Nginx由它来做域名识别、HTTPS 卸载、请求转发。这个架构的核心设计思想是“识别 转发”Nginx 根据请求头里的 Host 字段识别出用户访问的是哪个域名再根据配置决定走哪个证书、转发到哪个后端。这里有几个关键认知必须建立起来。第一个认知Nginx 是先在所有 server 块里选出一个匹配的虚拟主机然后才在这个虚拟主机的上下文里处理证书和转发规则。也就是说证书和域名的绑定关系、转发规则和域名的绑定关系都要放在同一个 server 块里而不是分散在多个地方。如果你把证书配置放在一个文件、转发配置写在另一个文件最后通过 include 拼在一起那就要确认它们的归属关系是否被正确嵌套。第二个认知HTTPS 的证书握手发生在 HTTP 请求解析之前。这个顺序极其重要直接决定了多证书配置的可行性——这么解释浏览器向服务器的 443 端口发起 TLS 握手时这个阶段 Nginx 根本还不知道用户要访问哪个域名因为 HTTP Host 头还没发过来它只能根据 SNI 扩展里的域名信息来选择用哪个证书。SNI 是 TLS 协议层的字段里面写了客户端想要访问的域名。Nginx 就是靠 SNI 把哪个域名对应哪张证书映射起来的。如果客户端不支持 SNI比如特别老旧的系统那多证书就无法按域名区分只能走默认证书兜底这个点在实际生产环境中要特别注意。第三个认知多服务不一定都要走端口区分。很多人一提到多个服务脑子里就是 service1 用 8080、service2 用 8081。但在 Nginx 的架构下更优雅的玩法是每个服务用独立域名或独立路径前缀对外暴露后端无论跑在哪个端口都没关系用户感知不到。这样做的好处是端口资源不浪费防火墙规则简单同时对外只有一个 80/443 入口安全性也更好控制。把这些认知理清了再看配置文件就不会懵。后面我会按顺序拆解从证书目录设计、核心配置参数、到多服务分流的几种方案最后把实战中踩过的坑列成清单。2. 准备工作证书目录设计与核心配置参数2.1 证书与配置文件的目录组织先解决文件放哪儿的问题。很多初学者把证书文件随便丢在 /tmp 或者某个服务的 home 目录下这是隐患很大的做法。证书和私钥的读取权限、备份策略、续期更新路径都应该提前规划好。我习惯的做法是这样的/etc/nginx/ ├── nginx.conf ├── certs/ │ ├── blog.codetime.cn/ │ │ ├── fullchain.pem │ │ └── privkey.pem │ ├── api.codetime.cn/ │ │ ├── fullchain.pem │ │ └── privkey.pem ├── conf.d/ │ ├── blog.conf │ ├── api.conf │ └── common.conf每个域名一个独立目录放证书好处是续期工具比如 acme.sh 或 certbot可以按目录刷新不用手动挪文件。每个业务域名的 server 配置单独放一个文件逻辑清晰排错的时候直接看对应文件就行不会在几百行的总配置文件里翻得头昏眼花。证书目录的权限要特别注意。私钥文件privkey.pem一旦泄露等于把该域名的 HTTPS 加密完全暴露给攻击者。我把 /etc/nginx/certs 目录的权限设置为 700私钥文件设置为 600属主是 root。如果 Nginx 的 worker 进程不是以 root 运行而是用普通用户比如 nginx 用户运行那需要让该用户对证书文件有读权限。通常的做法是保持文件属主 root同时把目录和文件的组设置为 nginx权限设为 640这样既安全又能正常读取。提示如果你使用 acme.sh 自动续期证书一定不要把证书文件直接生成在 Nginx 目录里由 Nginx 自发管理而是让续期工具更新证书后通过 --install-cert 命令复制到 Nginx 的证书目录并触发重载。这样证书和配置的耦合度低切换根证书或换 CA 的时候不用动 Nginx 配置。2.2 必须理解的三个关键参数配置文件里天天见的几个参数经常有人搞混它们的作用范围这里花点篇幅讲透。第一个是 server_name。它匹配的是请求头里的 Host 字段不是 IP。浏览器访问 https://blog.codetime.cn 的时候Host 头就是 blog.codetime.cn。Nginx 收到请求后会遍历所有 server 块找到 server_name 匹配的一个来处理这个请求。匹配优先级从高到低是精确匹配 最长的含通配符的匹配通配符以 * 开头 正则表达式以 ~ 开头 默认 serverlisten 指令里带 default_server 的那个。很多人配置了多个域名结果请求全部落到第一个 server 块上就是因为没设 default_server 或者优先级理解错了。第二个是 listen。对于 HTTPS标准写法是listen 443 ssl;在较新版本 Nginx 中也可以写成listen 443 ssl http2;或更推荐的写法listen 443 ssl; http2 on;另外一个容易踩坑的是 listen 中要不要添加 default_server。如果多个 server 块都监听 443但没有指定哪个是 default_serverNginx 默认使用文件中第一个出现的 server 块作为默认处理。访问一个未配置域名的 IP 或者一个没有匹配到任何 server_name 的域名时就会落到这个默认块上。所以建议给默认块配上 default_server并且写一个默认证书、返回 404 或 444防止别人直接用 IP 扫描你的服务器。第三个是 ssl_certificate 和 ssl_certificate_key。这两个指令定义了该 server 块使用的证书和私钥路径就是前面提到的 SNI 映射的关键。每个 server 块可以有自己的证书这就是“多证书”功能的实现基础。注意老版本的 Nginx 还支持 ssl_certificate 指令写在 http 上下文里作为全局兜底但每 server 块的证书会覆盖它。2.3 SSL 协议配置的版本选择与安全项很多生产事故不是域名或服务的问题而是 TLS 配置过时导致部分客户端访问异常。我在实践中用的基础配置模板如下ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off;这里解释几个选择的原因只保留 TLS 1.2 和 1.3因为 TLS 1.0/1.1 已经被协议层面废弃部分浏览器和支付类接口都要求禁用老版本。不规范但必须承认行业内确实还有人用特别旧的操作系统客户端比如 Windows 7 自带的浏览器如果遇到这类用户无法访问的情况在不影响安全合规的前提下可以临时放开 TLS 1.1但不建议长期保留。加密套件优先选择 ECDHE 开头的套件因为它们支持前向保密即使用户私钥泄露过去抓到的流量也无法被解密。这些参数建议写在一个单独的 common.conf 里然后在每个 server 块用 include 引入避免在多个配置文件里重复粘贴一大段。这个做法也保证了全局证书策略的一致性。3. 多域名多证书的完整配置实现3.1 HTTP 强制跳转 HTTPS一个已经决定启用 HTTPS 的站点就不应该再让用户通过 HTTP 明文访问。规范的做法是保留 80 端口的监听但它的唯一职责就是把请求 301 到 HTTPS 地址上去。跳转配置有两种组织方式。一种是每个业务一个跳转 server 块server { listen 80; server_name blog.codetime.cn; return 301 https://$host$request_uri; }另一种是统一在同一个 80 端口 server 块里做所有域名的跳转server { listen 80 default_server; server_name _; return 301 https://$host$request_uri; }从这个配置开始理解一个常见误区$host 和 $server_name 的差异。$host 是请求实际携带的域名$server_name 是配置里匹配到的server_name值。如果用 return 301 https://$server_name$request_uri在用户通过备用域名访问时会被跳到主域名上这往往不是我们想要的结果。所以建议使用 $host保持用户原始访问的域名不变只把协议从 HTTP 换成 HTTPS。3.2 单域名单证书的标准 server 块下面给出一个标准的、包含证书和多服务转发配置的单域名 server 块以博客服务为例server { listen 443 ssl; server_name blog.codetime.cn; include /etc/nginx/conf.d/common.conf; ssl_certificate /etc/nginx/certs/blog.codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/blog.codetime.cn/privkey.pem; root /var/www/blog; index index.html; location / { try_files $uri $uri/ 404; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; 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; } access_log /var/log/nginx/blog.log; error_log /var/log/nginx/blog_error.log; }这里有几个细节值得展开说。root 和 try_files 的组合是纯静态站点最常见的配置。try_files 的含义是依次尝试是否存在对应的文件文件不存在时匹配目录下的 index 文件如果都没有就交给 404 处理。这个写法避免了静态资源请求打到后端服务上造成无意义消耗。location /api/ 这一段是把动态请求反向代理到本地 8080 端口。proxy_pass 的地址后面带不带 /决定了 URL 的拼接方式后面我会专门展开讲这里先记住这个点。同时proxy_set_header 里设置了 X-Forwarded-For 和 X-Forwarded-Proto这样后端应用可以拿到用户真实 IP 和原始协议HTTP 还是 HTTPS很多框架的 URL 生成和日志分析都依赖这两个头。3.3 多域名共用一块 server 的写法如果你的多个域名承担的角色完全一致比如一个项目的国内访问域名和海外访问域名证书也是一张泛域名证书或者多个域名共用同一张证书那可以合并到一个 server 块里减少配置冗余server { listen 443 ssl; server_name codetime.cn www.codetime.cn; root /var/www/codetime; }注意同一张证书必须包含这两个域名才能这样用。如果是两张独立证书就必须拆成两个 server 块因为一个 server 块只能配一组 ssl_certificate 和 ssl_certificate_key。拆开后两个 server 块如果处理逻辑完全一样一种做法是共用同一段 location 配置把 location 部分提取到单独文件 include 进来或者使用 Nginx 提供的嵌入式变量配合正则匹配。坦白说这种场景不是特别常见因为大多数情况下不同域名都对应不同的业务服务拆开写反而更清晰。3.4 泛域名证书的配置差异如果团队申请的是 *.codetime.cn 的泛域名证书证书文件只有一张但网站的 server 配置还是可以按子域名拆开只是所有服务器块引用同一份证书文件server { listen 443 ssl; server_name blog.codetime.cn; ssl_certificate /etc/nginx/certs/codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/codetime.cn/privkey.pem; root /var/www/blog; }泛域名证书的优点是只要新增一个子域名不用重新申请证书直接把配置文件和目录建好就能用非常适合子域名多、上线节奏快的情况。缺点也很明显私钥泄露的影响范围更大一旦泄露整个域名的所有子站全部遭殃所以私钥的权限管理更要严格。另外泛域名证书一般不覆盖根域名 codetime.cn需要在证书申请时把根域名也加进 SAN 列表或者给根域单独配一张证书。3.5 HTTP/2 与证书的兼容性HTTP/2 在多域名多证书的场景下有一个冷门坑HTTP/2 协议要求使用 ALPN 协商而 ALPN 是 TLS 层的东西同一条 TCP 连接上的多个 HTTPS 请求不能换证书。也就是说同一个浏览器连接上如果同时访问你的两个不同证书的域名浏览器通常会为不同域名建立不同的 TCP 连接所以一般不冲突。但如果 HTTP/2 连接复用了同一个 TCP 连接且该连接上的域名对应的证书不同就会有一个请求失败、或者必须走默认证书。现实中浏览器对跨域名的连接复用是严格限制的所以这个坑大部分情况下不会触发但在某些自研客户端或者反向代理转发场景下可能遇到。解决办法也很简单确认每域名独立证书而不是让多个域名共用同一个 server 块并且靠 path 区分。如果你确实需要同一个 server 块下多个域名且域名对应不同证书那就只能拆开 server没有别的方案。重要提示配置完所有 server 块后执行 nginx -t 检查语法确认无误后再执行 systemctl reload nginx。reload 是平滑升级 Nginx 配置的方式不会中断现有请求真正测试过的配置可以放心 reload。如果 nginx -t 报错系统提示 nginx: [emerg] 这类字样那就是配置文件有问题必须修改后重新测试不要让配置文件处于坏状态否则 reload 不起作用。4. 多服务分流从端口隔离到域名与路径路由上一节解决了域名和证书的绑定问题这一节解决“服务”的拆分与转发问题。所谓多服务核心是 Nginx 的后端路由能力请求进来以后Nginx 要根据规则把请求转发到不同的后端进程。4.1 按域名分流最清晰的路由方式这是最推荐的多服务方案。每个服务独占一个域名Nginx 通过 server_name 就能完整区分转发到各自的后端。比如blog.codetime.cn → 博客服务后端 8080api.codetime.cn → API 服务后端 8081admin.codetime.cn → 管理后台后端 8082每个 server 块里的 location 配置互不影响一个服务的路径调整不会牵连其他服务。域名分流的另一个好处是证书、日志、限流、访问控制都可以按域来隔离。server { listen 443 ssl; server_name api.codetime.cn; include /etc/nginx/conf.d/common.conf; ssl_certificate /etc/nginx/certs/api.codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/api.codetime.cn/privkey.pem; location / { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; } }为什么这里 proxy_pass 没有带 URI即地址末尾没有斜杠和路径完整规则是这样的如果 proxy_pass 后面没有 URI则 Nginx 会原样传递请求的 URI不做任何改写比较安全。如果 proxy_pass 带了 URI比如 proxy_pass http://127.0.0.1:8081/;那么请求会以该 URI 替代 location 匹配到的部分来转发。这个区别极其重要也是新增多服务时最常见的错误来源。4.2 按路径分流一个域名承载多个服务有时候一个域名下就挂着多个服务比如 docs.codetime.cn/docs 是文档服务docs.codetime.cn/upload 是上传服务。这种场景下用 path 区分每个 location 对应一个后端。但要注意路径分流的耦合程度比域名分流高多个服务的入口共享同一个证书和域名配置错误的影响范围更大。server { listen 443 ssl; server_name docs.codetime.cn; include /etc/nginx/conf.d/common.conf; ssl_certificate /etc/nginx/certs/docs.codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/docs.codetime.cn/privkey.pem; location /docs/ { proxy_pass http://127.0.0.1:8090/; } location /upload/ { proxy_pass http://127.0.0.1:8091/; client_max_body_size 1024m; } }location /docs/ 匹配规则说明Nginx 会选择最长的 location 前缀作为匹配结果。比如请求 /docs/page/1会命中 location /docs/而请求 /doc 不会命中因为少了后面的斜杠那是另一个 location /doc 才处理的范围。所以配置路径分流时前后的斜杠必须严格保持一致少一个斜杠会导致请求落到默认的 location / 上行为完全不可预期。client_max_body_size 单独写在 upload 服务上控制上传文件的大小限制默认是 1m不调整的话超过 1MB 的上传直接报 413 错误。很多人配置完上传接口后发现大小的文件传不上去就是漏了这一项。4.3 按端口分流Nginx 作为多服务代理的过渡方案用不同端口区分不同服务是很多内部系统常用的方式前端 80/443 入口是 Nginx后端服务按照端口号服务 A 用 8080服务 B 用 8081分配给不同的服务。Nginx 通过 server_name 和端口组合可以在同一个域名下用不同端口访问不同服务。server { listen 8081 ssl; server_name codetime.cn; ssl_certificate /etc/nginx/certs/codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/codetime.cn/privkey.pem; location / { proxy_pass http://127.0.0.1:9001; } }这种方式虽然也能工作但不建议作为对外服务的主方案理由是开放的端口越多暴露面越大而且大部分用户不习惯在 URL 里带端口访问连接可能被防火墙拦截同一个域名或 IP 上的证书是同一张无法按端口区分证书。更重要的是一旦服务数量增长端口管理会迅速失控。所以端口分流更适合做临时方案或者内网接口对外服务的多域名架构应该以域名分流为核心。4.4 WebSocket 与长连接服务的反向代理配置多服务场景里难免会有 WebSocket 服务比如实时通知、在线聊天、协同编辑。Nginx 默认的 HTTP 代理不处理 WebSocket 协议升级的请求头所以需要显式配置。location /ws/ { proxy_pass http://127.0.0.1:8092; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 86400s; }解释一下这段配置的逻辑WebSocket 连接建立的第一步是 HTTP 请求携带 Upgrade 和 Connection 头来升级协议。Nginx 默认转发 HTTP 时Connection 头被设为 close导致升级失败。这里把 $http_upgrade请求中携带的 Upgrade 头转发给后端同时强制 Connection 为 upgrade让后端能够正确建立 WebSocket 通道。proxy_read_timeout 设置成 86400 秒24小时是为了防止正常的 WebSocket 长连接在空转时被 Nginx 默认的 60 秒超时掐断。这也是很多人配了 WebSocket 后发现“连接一会就断”的原因超时时间没调大。4.5 静态资源与动态服务的动静分离多服务架构中经常有一种情况一个域名下静态资源图片、CSS、JS 文件直接由 Nginx 读磁盘返回动态接口请求才转发到后端。动静分离的意义是减轻后端压力因为后端语言处理静态文件的性能远不如 Nginx 这种事件驱动的模型。location ~* \.(png|jpg|jpeg|gif|css|js|ico|svg|woff|woff2)$ { root /var/www/static; expires 30d; add_header Cache-Control public, no-transform; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; }正则 location 匹配到这些静态文件后缀时直接从静态目录返回文件并带上长缓存头。这里专门说一说 expires 30d 的作用浏览器收到这个响应头后30 天内请求同一路径不会再向服务器发请求直接读本地缓存。这个策略对页面加载速度的提升非常明显但在发布新版本时要注意给静态文件加上版本号或 hash否则用户拿到的是旧缓存。注意静态资源目录不要和项目代码目录混在一起。发布新代码时如果不清除旧静态文件就可能出现残留的历史文件被旧 URL 命中导致安全或数据污染问题。小团队常见的做法是把静态资源放在 /var/www/static每次发布前同步代码或做符号链接切换。5. 实操过程全记录从零到一个生产可用配置5.1 一次完整的多域名配置演示下面我们把上面讲的所有点串起来模拟一个实际场景一台新服务器要部署三个服务——博客静态站点 少量动态接口、API 服务、管理后台且分别使用三个域名。我按照实际操作顺序记录整个流程。第一步安装 Nginx。Ubuntu 系统示例apt update apt install nginx nginx -v第二步创建目录结构mkdir -p /etc/nginx/certs/blog.codetime.cn mkdir -p /etc/nginx/certs/api.codetime.cn mkdir -p /etc/nginx/certs/admin.codetime.cn mkdir -p /var/www/blog第三步编辑主配置 /etc/nginx/nginx.conf确保 http 块内有 include conf.d/*.conf这个默认就有不过要确认没有冲突的 include 路径。第四步编写公共配置 /etc/nginx/conf.d/common.confssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security max-age31536000; includeSubDomains always; add_header X-Frame-Options SAMEORIGIN always; add_header X-Content-Type-Options nosniff always;这里加上了 HSTS 和几个常见安全响应头。Strict-Transport-Security 的作用是告诉浏览器“以后只能通过 HTTPS 访问本站”max-age 设一年。这个头一旦生效浏览器会自动把 HTTP 请求重写成 HTTPS即使你的 80 端口跳转服务出故障用户也没有机会走明文连接。不过要注意HSTS 生效前应该确认所有子域都支持 HTTPS否则子域访问会直接失败。第五步编写博客域名的配置 /etc/nginx/conf.d/blog.confserver { listen 80; server_name blog.codetime.cn; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name blog.codetime.cn; include /etc/nginx/conf.d/common.conf; ssl_certificate /etc/nginx/certs/blog.codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/blog.codetime.cn/privkey.pem; root /var/www/blog; index index.html; location / { try_files $uri $uri/ 404; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; 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; } access_log /var/log/nginx/blog.log; }第六步编写 API 域名配置 /etc/nginx/conf.d/api.confserver { listen 80; server_name api.codetime.cn; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name api.codetime.cn; include /etc/nginx/conf.d/common.conf; ssl_certificate /etc/nginx/certs/api.codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/api.codetime.cn/privkey.pem; location / { proxy_pass http://127.0.0.1:8081; 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; } location /ws/ { proxy_pass http://127.0.0.1:8082; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 86400s; } }把 API 的 REST 服务和 WebSocket 服务放在同一个域名下通过 /ws/ 路径区分实践上很常见。这里 API 的 REST 部分走 8081WS 部分走 8082。第七步编写管理后台域名配置 /etc/nginx/conf.d/admin.confserver { listen 80; server_name admin.codetime.cn; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name admin.codetime.cn; include /etc/nginx/conf.d/common.conf; ssl_certificate /etc/nginx/certs/admin.codetime.cn/fullchain.pem; ssl_certificate_key /etc/nginx/certs/admin.codetime.cn/privkey.pem; # 管理后台只允许内网 IP 或跳板机访问 allow 10.0.0.0/8; allow 192.168.1.0/24; deny all; location / { proxy_pass http://127.0.0.1:8083; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } access_log off; }管理后台的配置多了访问控制。很多团队的管理后台挂在公网上被扫描爆破教训惨痛。用 allow/deny 做 IP 白名单是最简单粗暴的方案内网办公网段放行其他 IP 一律拒绝。如果团队在外办公需要用动态 IP 访问建议改为在 Nginx 层做 HTTP Basic Auth 或 OAuth 认证至少加一层防护。第八步检查语法并重载nginx -t systemctl reload nginxngnix -t 会检查所有 include 进来的配置文件语法有错误会直接带行号和具体错误信息提示。确认通过后执行 reload这个操作是平滑重载对正在处理的请求没有影响。我一般习惯在 reload 之后立刻访问一次所有域名确认响应头、证书链、转发路径都正常。5.2 reload 和 restart 的区别这里说一个很多初学者搞不清楚的点修改 Nginx 配置后到底用 reload 还是 restart。reload 是让主进程重新读取配置文件并逐步替换 worker 进程期间不中断现有连接是绝对推荐的方式。restart 是完整停止再启动所有连接都会断开用户正在下载的文件会中断已经建立的 WebSocket 连接全部重连在线上环境属于“时刻拿起又放下”的高危操作。所以改配置一律选择 reload。只有当 Nginx 的二进制版本升级或者模块加载变化时才需要 restart通常表现为 nginx 服务整体无法 reload 时才这么做。5.3 证书申请与自动续期的接入证书文件本身怎么获取不展开讲每一步但续期这个环节必须说明白。以 acme.sh 为例给 blog 域名申请证书的命令大致是export DEPLOY_NGINX_RELOADsystemctl reload nginx acme.sh --issue -d blog.codetime.cn --nginx acme.sh --install-cert -d blog.codetime.cn \ --key-file /etc/nginx/certs/blog.codetime.cn/privkey.pem \ --fullchain-file /etc/nginx/certs/blog.codetime.cn/fullchain.pem \ --reloadcmd systemctl reload nginx这样配好以后续期工具会在证书到期前自动更新证书文件并触发 reload全程无人工干预。注意顺序先 --issue 签发成功再 --install-cert 安装到 Nginx 证书目录最后用 --reloadcmd 完成 reload。如果证书文件路径和配置不对应续期后 Nginx 执行的 reload 会加载新证书但因为路径错误加载到旧的或者不存在的文件可能直接起不了服务。一旦发现这种情况最快的处理办法是检查 install-cert 生成的路径是否和 server 块里的 ssl_certificate 路径完全一致。6. 高频问题与排查思路这部分内容来自实际运维中反复出现的问题。我把常见故障和排查方法整理成一张速查表。现象最可能原因排查方向访问域名显示 Nginx 默认欢迎页请求没有匹配到你的 server 块落到了默认站点检查 server_name 是否多打字母、检查 default_server 落在哪个文件HTTPS 访问提示证书无效证书文件过期、域名 SAN 不包含该域名、证书链不完整用浏览器开发者工具或 openssl s_client 查看证书链多个 HTTPS 域名只有一张证书生效多个 server 块没有写 ssl_certificate全部走了默认证书检查每 session 块的证书路径配置HTTP 访问变成 HTTPS 后提示太多重定向80 跳 443 和 443 内部强制跳转叠加查 rewrite 或 return 301 的循环页面显示正常但 API 请求 404proxy_pass 路径拼接问题重点检查 proxy_pass 后面是否带了 URI 和斜杠WebSocket 连上后立即断开proxy_read_timeout 未设置或太小调大超时时间上传文件提示 413client_max_body_size 未设置在对应 location 增加上传大小限制访问未配置的域名返回了一个业务站点默认 server 未配置落到第一个 server单独建 default_server 拒绝未知域名6.1 证书无效问题的深层解析证书无效的问题在所有 HTTPS 排障里占比最高。浏览器提示“证书无效”时先分清楚是哪种无效缺少中间证书链是最容易被忽略的。你申请到的证书通常包含两部分网站证书leaf certificate和中间证书intermediate certificate。Nginx 配置里要填写的是同时包含这两部分内容的 fullchain 文件有时叫 fullchain.pem而不是单独的 cert.pem。如果只填了网站证书浏览器因为无法验证它和根证书之间的信任链会直接判定为无效。排查方法openssl s_client -connect blog.codetime.cn:443 -servername blog.codetime.cn这个命令会输出服务器返回的完整证书链。如果输出里只有一级证书中间证书缺失那问题就在 fullchain 文件里如果输出里有完整的证书链继续看证书的域名匹配部分。域名不匹配的情况也常见证书 SAN 列表里写的是 blog.codetime.cn但你用 www.codetime.cn 去访问浏览器直接报错。泛域名证书只能匹配一个层级*.codetime.cn 能匹配 blog.codetime.cn但不能匹配 api.blog.codetime.cn那是二级泛域名才覆盖的。6.2 重定向死循环的排查思路重定向循环ERR_TOO_MANY_REDIRECTS通常并不复杂原因大多是多个跳转规则叠加。常见场景HTTP 跳到 HTTPS80 端口 return 301然后 HTTPS 的 server 块里又有 rewrite 规则把 http 请求再跳一次死循环就产生了。排查方法是先访问 http://blog.codetime.cn用浏览器开发者工具查看网络请求看重定向链路上每个 301 的 Location 地址。如果看到 Location 一直指向自身或者来回两个地址互指就能定位到是哪一层跳转出了问题。另一个常见场景是后端应用的强制 HTTPS 开关比如某些框架配置了 SECURE_PROXY_SSL_HEADER 或者 force_sslNginx 已经卸载了 HTTPS转发到后端的却是 HTTP 协议后端一看协议不是 HTTPS又给你 301 回 HTTPS循环就产生了。解决办法是在 Nginx 转发时带 X-Forwarded-Proto $scheme并且在框架里信任这个头。6.3 正则 location 的优先级陷阱Nginx location 匹配分为前缀匹配和正则匹配复杂的规则混合使用时特别容易踩坑。简单规则是这样的先进行普通前缀匹配记住最长的匹配结果然后开始按顺序检查正则表达式一旦有正则匹配成功直接使用这个正则的规则前面的前缀匹配结果作废。举个例子location /images/ { root /var/www/static; } location ~* \.png$ { root /var/www/other; }请求 /images/logo.png 会命中后面这个正则规则而不是前面这个 /images/ 前缀规则。如果你原本期望的是 /images/ 下面的图片走 static 目录那就需要在正则里排除掉 /images/ 路径或者用 ^~ 修饰符固定前缀匹配优先location ^~ /images/ { root /var/www/static; }^~ 的含义是一旦匹配到这个前缀不再继续检查正则表达式。但滥用 ^~ 也会导致部分动态路径无法命中正常的正则规则使用时要想清楚业务形态。6.4 default_server 缺失导致的“串门”事件多个域名共用同一台服务器如果缺失 default_server 配置用户访问一个未关联任何业务域名的域名比如被别人解析到你的服务器 IPNginx 会默认把它分配到第一个监听该端口的 server 块。如果你的第一个 server 块恰好是后台管理系统这个未关联域名就直接暴露了后台登录页配合弱口令爆破灾难就是这么产生的。正确做法是单独建一个默认拒绝的 server 块server { listen 80 default_server; listen 443 ssl default_server; server_name _; ssl_certificate /etc/nginx/certs/default/fullchain.pem; ssl_certificate_key /etc/nginx/certs/default/privkey.pem; return 444; }return 444 是 Nginx 的非标准响应码它的效果是“直接关闭连接”不返回任何内容。使用默认证书可以是自己签的自签名证书是为了让 TLS 握手能进行到 HTTP 阶段然后才执行 return 444。如果连默认证书都没有TLS 握手阶段就会报证书错误虽然也能挡住访问但会留下更多杂音和扫描关注。有了这个默认块再也不用担心 IP 直访或绑定未知域名访问到你的真实业务。7. 我实际用下来的一些体会写到这里多域名多证书多服务的 Nginx 配置框架基本完整了。这套方案我在很多项目里反复用过最深刻的体会是配置本身不难难的是对 Nginx 匹配顺序、证书加载时机、代理转发细节这些底层逻辑有清晰认知。我踩过的几个值得分享的坑分别是第一每当要新加一个域名时我现在的习惯是先写好配置再 cat 一下确认证书路径真实存在然后 nginx -t最后 reload。这四步顺序一次都不能乱。有几次因为急着上线跳过 nginx -t 直接 reload结果配置里有变量引用错误服务直接被 reload 搞挂——这个教训是硬碰硬得出的。第二证书续期如果使用自动工具不要只盯着“是否续期成功”要确认续期后的证书真的被 Nginx 加载了。最简单的方法是在续期后的第二天用浏览器或 openssl 命令检查一下证书的生效时间和到期时间确保 server 块配置里的证书路径和续期工具写入的路径一致。曾经遇到一台机器的 acme.sh 续期正常但证书写到了临时目录Nginx 一直加载几个月前的旧证书直到浏览器大面积报警才排查出来整个过程非常被动。第三多服务的拆分尽量以域名为主线一个域名对应一个唯一的对外入口后端端口合理规划写入文档。路径分流适合内部或临时需求用多了会让配置的可读性和可维护性迅速下降。等到项目演进、人员更替之后新接手的同事看到一堆嵌套的 location 规则排查问题的成本会成倍增加。这套配置方案还有一个很自然的扩展方向如果后端服务容器化Nginx 的 upstream 可以从固定的 IP:端口改成一组容器实例的动态列表配合健康检查自动摘除故障节点。在这个基础上多域名多证书的框架完全不需要改动只需要把 proxy_pass 的目标从单点地址改成 upstream 组名即可。所以先把这一层基本功打扎实后面的扩展会顺手很多。最后再分享一个小技巧在 /etc/nginx/conf.d 下我习惯把所有文件名按数字前缀编号比如 10-blog.conf、20-api.conf、30-admin.conf并且建一个 00-default.conf 放默认拒绝块。这样可以确保 include 的顺序可控默认块永远排最前面后面业务配置再按编号递增。配置多了以后这个目录就是整个服务器服务拓扑的说明书浏览一遍文件名就能说出这台机器挂了哪些服务。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。