做Nginx配置做了这么多年,我发现一个很有意思的现象:凡是涉及多域名、多证书、多服务的需求,十个人里有八个人会在同一批地方栽跟头。我自己也不例外,早期在一台服务器上同时挂一个官网、一个管理系统、一个API,结果证书选错、请求404、WebSocket连不上轮番出场,折腾到凌晨才把配置理顺。
这次要分享的是一套我实测过、可以直接改改域名就用起来的Nginx配置方案,涵盖三块核心能力:多域名入口、每域名独立SSL证书、按域名或路径把请求转发给多个后端服务。文章里会拆解server_name的匹配规则、SNI选证书的原理、proxy_pass分流的设计思路,以及我踩过的几个真实坑。适合刚把多项目往一台服务器上收敛的同学,也适合已经会配但总在某个边角上出问题的人。
先给个结论:这套方案的最终形态不复杂,但每一步选型的理由和背后的匹配逻辑才是真正值钱的部分。读完你不仅能照抄配置,还能知道当Nginx行为不符合预期时该往哪个方向排查,而不是靠瞎试。
1. 先盘清楚需求:多域名多证书多服务到底要解决什么
1.1 三个“多”分别对应的问题
讲配置之前,先把混乱的根源盘清楚。
“多域名”解决的是入口统一问题。以前一台服务器想跑多个项目,最粗暴的办法是不同端口:80给官网、8080给后台、3000给API。但端口的记忆成本高、被防火墙拦的风险大,用户体验也差。域名才是天然的分流标签,a.example.com、b.example.com、c.example.com各自指向同一台服务器,Nginx在HTTP层就完成分发,用户只需要记一个干净的域名。
“多证书”解决的是信任独立问题。早期有人图省事把两个域名塞进同一张SAN证书里,一张证书列多个域名。SAN方案能用,但每次证书到期所有域名一起续,加新域名还得重新签发;更麻烦的是如果一个域名因为备案或其他原因需要从证书里移除,整张证书都得换,波及其他完全不相干的站点。所以现在的主流做法是每个域名一套独立证书,到期各自续签,互不干扰。
“多服务”解决的是应用解耦问题。一台服务器上可能跑着Node.js的API、Java的管理端、Python的定时任务后台。每个服务独立进程、独立端口,但对外全部收口到Nginx的80/443,由Nginx统一做路由和转发。这样做的好处是后端服务可以随时换端口、换机器,对外地址不变,平滑迁移。
1.2 端口分流 vs 域名分流的方案对比
两种分流方案我都用过,简单做个对比:
| 维度 | 端口分流 | 域名/SNI分流 |
|---|---|---|
| 访问方式 | 域名后还要带端口号 | 直接用标准端口,无感 |
| 证书管理 | 每端口可以独立证书 | 同一443端口靠SNI选证书 |
| 防火墙兼容性 | 高风险端口容易被拦 | 只需放行80/443 |
| 后端升级影响 | 改端口影响用户书签 | 后端改端口不影响外网地址 |
| 配置复杂度 | 低,适合临时环境 | 略高,但一次配好长期受益 |
结论很直接:长期对外服务,尤其是要上HTTPS的,别走端口分流的老路。域名分流虽然一开始配置要细心一点,但换来的是干净的入口和灵活的后端部署方式,这点时间投入完全值得。
2. server_name 匹配规则:Nginx按什么路径认出你的域名
2.1 四种匹配方式与优先级判定
这是很多人配置出错的第一现场。Nginx收到请求后,必须从一堆server块里挑出一个来处理,挑选的依据就是HTTP请求头里的Host字段。注意HTTPS场景下,TLS握手阶段还有一个SNI字段会先参与选证书,选完证书之后HTTP层还是会按server_name再匹配一次处理逻辑,两层是独立的,后面细讲。
server_name的匹配规则可以分成四类:
- 精确匹配:比如 server_name a.example.com;
- 通配符前缀:*.example.com,匹配 a.example.com,不匹配 b.a.example.com;
- 通配符后缀:www.example.*,匹配 www.example.com、www.example.cn 等;
- 正则表达式:~^www\d+.example.com$,用正则做更灵活的匹配。
匹配时Nginx按这个优先级来:
- 优先精确匹配;
- 然后是前缀通配符,按匹配长度最长的胜出;
- 再是后缀通配符,同样按最长匹配;
- 最后才是正则,按server配置文件中出现的先后顺序,先到先得。
这个顺序非常容易踩坑。比如你同时配了 server_name *.example.com 和 server_name ~^www.example.com$,www.example.com 的请求会被通配符那条接走,正则根本没机会执行——因为通配符的优先级更高。别指望正则的“精确”能在这种较量里获胜。
另一个高发坑是空Host请求。有些爬虫或老旧客户端不发Host字段,此时Nginx会一律交给默认server处理。如果你的非默认server里没有对这种边界情况做保护,裸IP访问或空Host请求就会命中某个你根本不想暴露的站点。
2.2 默认server的特殊性与配置建议
默认server的选定规则是:listen 443 ssl default_server 所在的那个server块胜出,如果没有显式写default_server,那么监听同一IP加端口的第一个server块就是默认server。
我见过不少生产事故,就是因为“没写default_server,以为没有默认server”而翻车的。事实上只要不写,第一个出现的就会被当作默认,用户用IP直接访问、或者SNI传了一个你根本没配置的域名,请求就会落到这个“意外的默认server”上。证书不匹配的浏览器警告就是这么来的。
建议是:
- 显式写一个 default_server 的server块,配一张自签名证书或者任意兜底证书,直接返回444或跳转到一个提示页;
- 所有业务域名都写清楚server_name,不要依赖位置靠前;
- 裸IP访问直接return 444,不返回任何内容,让它自然断连。
这里补一句:server_name配置不建议写太宽泛的泛域名,尤其是你手上同时有多个不相关业务的时候。一个 *.example.com 的泛匹配可能把另一个 example.net 的请求也卷进来,后面排查起来极其费劲。宁可多写几个明确域名,也别图省事。
3. SNI 多证书的核心原理:一张443端口挂多张证书
3.1 TLS握手顺序与SNI的作用
先回答一个常见疑问:同一个443端口怎么可能同时挂多张证书?TCP连接在同一个端口上只能有一个服务端,明明只能存在一份要返回的证书,不同域名怎么拿到不同证书?
答案是SNI(Server Name Indication,服务器名称指示)。TLS握手的第一步是ClientHello,客户端在这个包里会带上它要访问的域名,这个字段就叫SNI。Nginx在握手阶段就能从ClientHello里读到SNI,凭它去选对应的证书以及对应的server块。
可以类比酒店入住:每个住客在前台报自己的名字,前台凭名字找出对应的房卡。不报名字的人只能拿到一张默认房卡——也就是默认server的证书。这也解释了为什么早期HTTPS时代同一个IP只能放一张证书,因为那时客户端根本不会在握手时自报域名,服务器无从选择。现在浏览器和主流开发库都支持SNI,所以多证书方案才成为普遍做法。
注意一点:SNI只在握手阶段选证书,它不等于最终的server匹配。握手完成后,HTTP请求里的Host头才会决定请求最终进哪个server块处理。两者在实践中建议保持一致,否则可能出现证书选对了但业务进错了server的诡异现象。
3.2 多证书配置的具体写法
配置层面,多证书的写法其实非常朴素:每个有独立证书的域名,单独一个server块,里面指定自己的ssl_certificate和ssl_certificate_key。Nginx在握手阶段按SNI找到对应的server块,加载对应证书完成握手。
一个最小示例:
# 站点A:a.example.com server { listen 443 ssl; server_name a.example.com; ssl_certificate /etc/nginx/certs/a.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/a.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; } # 站点B:b.example.com server { listen 443 ssl; server_name b.example.com; ssl_certificate /etc/nginx/certs/b.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/b.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; }如果你的Nginx版本较新,HTTP/2的配置方式要注意一下。我用的Nginx版本写法是:
listen 443 ssl; http2 on;而在更早的Nginx版本里,写法是直接在listen上挂http2参数:listen 443 ssl http2;。两个写法在Nginx 1.25.1之后已经统一为 http2 on;,老写法虽然还能兼容,但新配置建议直接用http2 on,避免后面升级到新版本时出现重复指令问题。
3.3 证书续期自动化:省心但容易翻车
证书到期续期是比首次签发更容易出问题的地方。我见过最多的情况是:证书明明续期成功了,但Nginx还在用旧证书,原因就是续期后没有执行nginx -s reload。ACME客户端只管替换证书文件,Nginx的worker进程要reload之后才会重新加载证书。
另外,多证书续期的验证方式要匹配证书的签发方式:
- 用webroot模式签发的证书,续期时ACME客户端会往 /.well-known/acme-challenge/ 目录写验证文件。如果你的站点恰好没有把这个路径交给Nginx静态目录,验证就会失败。需要在对应server块里加一个location:
location ^~ /.well-known/acme-challenge/ { root /var/www/acme; }- 用DNS API模式签发的证书,续期时不依赖Web目录,但要确保ACME客户端里配置的DNS API凭据还有效。密钥轮换后忘了更新,续期会在验证阶段卡住,而且报错信息往往很隐晦,只说“验证失败”。
我的建议是不管用哪种方式,都配一个续期后自动reload的钩子。比如acme.sh的renew-hook,certbot的deploy hook,续期成功先nginx -t,通过了再reload。这样证书文件更新了,Nginx也就跟着换上新证书,不用每次手动介入。
4. 多服务分流配置:location、proxy_pass与upstream的配合
4.1 按域名分流还是按路径分流
证书问题解决了,接下来是一台服务器上的多个服务怎么对外提供的问题。我在实战里一般只保留两种分流模型。
按域名分流:每个域名代表一个独立站点或独立产品线,整个server块内部就是一个转发单元。比如 www.example.com 转发给前端静态服务,api.example.com 转发给Node.js API,互不干扰。
按路径分流:同一个域名下不同路径对应不同服务,比如 api.example.com/user 走用户服务,api.example.com/pay 走支付服务。这种模式对协议规范化要求更高,一般适合单域名下的大型单体应用拆分子服务。
两种模型共同的关键点是:转发目标的变化要尽量收敛在Nginx一层。后端服务的监听地址和端口可以随意调整,只要upstream里的配置跟着改,对外完全不感知。这就给了运维很大的自由度,后端要扩容、要迁移机器,动Nginx配置即可。
4.2 一套可直接改用的完整配置
我把三个场景合成一个完整配置示例:一个静态官网、一个Node.js API、一个带WebSocket的管理端,分别绑定不同域名。
# upstream定义后端服务地址 upstream backend_website { server 127.0.0.1:8081; # 静态官网 } upstream backend_api { server 127.0.0.1:3000; # Node.js API } upstream backend_admin { server 127.0.0.1:8090; # 管理端(含WebSocket) } # HTTP入口,统一跳HTTPS server { listen 80; server_name www.example.com api.example.com admin.example.com; return 301 https://$host$request_uri; } # 官网:转发到静态服务 server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/certs/www.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/www.example.com/privkey.pem; location / { proxy_pass http://backend_website; 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; } } # API:转发到Node服务 server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/certs/api.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/api.example.com/privkey.pem; location / { proxy_pass http://backend_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; } } # 管理端:转发到带WebSocket的Java服务 server { listen 443 ssl; server_name admin.example.com; ssl_certificate /etc/nginx/certs/admin.example.com/fullchain.pem; ssl_certificate_key /etc/nginx/certs/admin.example.com/privkey.pem; location / { proxy_pass http://backend_admin; 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; # WebSocket协议升级依赖的头部 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }这里有个新手容易绕晕的点:upstream块和server块分别扮演什么角色。其实Nginx的处理逻辑很简单——server块负责接客(接收外部请求),upstream块定义后端服务清单,proxy_pass指定把请求丢给哪个upstream。剩下的就是设置Host和真实IP等转发头,让后端能识别用户身份和来源。
4.3 转发头与WebSocket等细节
proxy_set_header这组指令是反向代理最容易忽略也最容易出问题的部分。后端拿不到正确的Host头,会产生两个典型问题:一是后端生成的一些跳转链接变成后端内网地址,用户在浏览器里点开直接访问内网IP;二是后端基于域名的逻辑判断完全失效。所以 Host $host; 基本是必须项。
X-Real-IP和X-Forwarded-For是用来把真实客户端IP传给后端的。很多场景下不配这组头,后端永远只能看到一个127.0.0.1,日志排查和基于IP的限制逻辑全部废掉。尤其做风控或限流的时候,拿不到真实IP等于白搭。
还有WebSocket这条,初次用Nginx转发WebSocket的人几乎都会在连接握手阶段卡住:页面能加载,WebSocket一直pending,最终失败。那是因为WebSocket握手依赖协议升级,Nginx默认不转发Upgrade头,必须在location里显式声明:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";另外注意proxy_read_timeout。WebSocket是长连接,默认的60秒超时会让空闲连接被早早切断,做管理端这种需要长时间在线的场景,把超时调大到3600秒是必要的。
5. 上线前验证与踩坑排查:我这边的实测过程
5.1 验证工具与命令
配置写完之后,上线之前的验证过程我基本固定这一套。先做语法检查:
nginx -t这个命令会检查所有配置文件语法。如果配置里有路径错误、括号不匹配,这里会直接报出来。但nginx -t只能证明语法没错,证明不了行为正确。接下来我会用curl做细化测试:
curl -v https://a.example.com --resolve a.example.com:443:服务器IP--resolve参数可以跳过DNS直接指定IP,这样在域名解析还没切换过来时就能先行验证。看输出的SSL证书Subject和Issuer,确认返回的证书是这个域名对应的那张。这一步能同时验证“SNI选证书”和“server匹配”两个环节。
再细一点,用openssl验证SNI选证书是否正确:
openssl s_client -connect 服务器IP:443 -servername a.example.com如果在返回里看到证书Subject是a.example.com,说明SNI选证书正常;如果看到的是其他域名的证书,说明默认server配错了或者SNI没生效。
5.2 常见的坑和解决
整个过程中我踩过的坑,筛几个值得讲的。
第一个坑:证书文件路径和权限。证书目录如果用了700权限,Nginx的worker进程没权限读,启动时报错或者reload失败。这个问题排查起来很容易被忽略,因为nginx -t只会提示“无法打开文件”或者权限不足。把证书目录设为750、证书文件至少640,放/etc/nginx/下并确认nginx用户可读写就好。
第二个坑:证书链不完整。很经典的错误——申请证书后只把服务器证书贴进去,没有把中间证书拼上。PC浏览器可能只是警告,但很多移动端或老客户端会直接报SSL错误。把fullchain.pem(完整证书链)和privkey.pem分开配置,是最稳妥的做法。
第三个坑:多证书环境的会话缓存与性能。高并发场景下每张证书的握手开销不小,配置里加 ssl_session_cache shared:SSL:10m; 可以让同一客户端的重复握手快得多。我在实测中把HTTPS连接建立时间从七百多毫秒压到三百毫秒以下,这个缓存起了很大作用。真别小看这一行配置,高并发时它能实打实省掉大量CPU开销。
第四个坑:续期脚本和Nginx reload的时序问题。我用的ACME客户端续期后默认会自动执行一次nginx -s reload,但如果你手动改过Nginx配置,就会遇到“续期脚本说reload成功了,但配置还在用旧版本”的错觉。实际上还是建议续期脚本里先跑nginx -t再reload,并把reload的日志单独落盘,出问题能第一时间看到失败原因。
我这里还专门碰到过一个和SNI相关的疑难杂症:某天用户反馈某个域名经常出现证书错误的间歇性报错。用openssl测了好多次都没复现,后来才发现是和另一个域名的server块互换了证书路径。因为Nginx加载证书是按server块逐个来的,配置里不小心把a的证书路径写到了b的server块,而a本身没有显式default_server,被那个意外的默认server接管了。这种错误nginx -t检测不出来,必须靠命令行验证逐个域名确认证书Subject才能暴露。自此之后,我每次多证书配置完都会把所有域名过一遍openssl验证,这已经成了肌肉记忆。
5.3 一些能提升体验的小配置
有几个配置不是必须但确实能让整体体验上一个台阶,我实际用下来都很满意:
- gzip和HTTP/2一起开。静态资源多的情况下,HTTP/2的多路复用让页面首屏明显变快。
- 针对不同后端类型设置不同的proxy_read_timeout。API接口常规60秒,WebSocket长连接3600秒,不要一刀切。
- 高频API场景可以给对应server块设置独立access_log,甚至按请求量决定是否关闭日志,减少磁盘IO压力。
gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1024;这段配置放在http块里就能生效。注意gzip_types别漏了application/javascript和application/json,API场景这两个类型是最常见的。
结尾
Nginx这套多域名多证书多服务配置梳理下来,核心其实就一句话:两层匹配机制决定了整体行为——TLS握手阶段按SNI选证书,HTTP阶段按Host选server块,两层都配清楚了,剩下的就是转发细节。
我个人的体会是,这类配置最大的成本不在写对配置本身,而在验证和排错的方法。多花十分钟把每个域名的证书、每个域名的转发目标都过一遍openssl和curl验证,远比出了问题再去翻日志高效。配置文件的注释里我也习惯把“这个server块负责什么域名、证书放哪、后端指向谁”写清楚,半年后再回来改配置,不至于靠猜。
最后分享一个小习惯:每次改完Nginx配置,我都先把旧配置文件cp一份带时间戳的备份,再动editor,这样即使改废了也能秒回滚。多域名环境里的server块一旦多了,手滑的概率是呈指数上升的,备份是成本最低的保险。下次遇到“某个域名证书不对”“某个接口后端一直拿到127.0.0.1”“WebSocket连不上”这类问题,按这个思路去查:先析SNI,再看Host,最后查转发头,基本都能在五分钟内定位。