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 匹配的一个来处理这个请求。匹配优先级从高到低是:精确匹配 > 最长的含通配符的匹配(通配符以 * 开头)> 正则表达式(以 ~ 开头)> 默认 server(listen 指令里带 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_server,Nginx 默认使用文件中第一个出现的 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 → 博客服务,后端 8080
- api.codetime.cn → API 服务,后端 8081
- admin.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.conf:
ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security "max-age=31536000; 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.conf:
server { 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.conf:
server { 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 部分走 8081,WS 部分走 8082。
第七步,编写管理后台域名配置 /etc/nginx/conf.d/admin.conf:
server { 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_RELOAD='systemctl 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 请求 404 | proxy_pass 路径拼接问题 | 重点检查 proxy_pass 后面是否带了 URI 和斜杠 |
| WebSocket 连上后立即断开 | proxy_read_timeout 未设置或太小 | 调大超时时间 |
| 上传文件提示 413 | client_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 跳到 HTTPS(80 端口 return 301),然后 HTTPS 的 server 块里又有 rewrite 规则把 http 请求再跳一次,死循环就产生了。
排查方法是先访问 http://blog.codetime.cn,用浏览器开发者工具查看网络请求,看重定向链路上每个 301 的 Location 地址。如果看到 Location 一直指向自身或者来回两个地址互指,就能定位到是哪一层跳转出了问题。另一个常见场景是后端应用的强制 HTTPS 开关(比如某些框架配置了 SECURE_PROXY_SSL_HEADER 或者 force_ssl),Nginx 已经卸载了 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 配置,用户访问一个未关联任何业务域名的域名(比如被别人解析到你的服务器 IP),Nginx 会默认把它分配到第一个监听该端口的 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 的顺序可控,默认块永远排最前面,后面业务配置再按编号递增。配置多了以后,这个目录就是整个服务器服务拓扑的说明书,浏览一遍文件名就能说出这台机器挂了哪些服务。