
做Nginx配置也有不少年头了每次碰到$http_host、$host、$proxy_host这三个变量总会有人问到底该用哪个。尤其是刚接触Nginx的运维和开发写proxy_set_header Host的时候全靠复制粘贴出了问题也不知道为什么。这三个变量看起来差不多实际上它们的来源、取值时机、适用范围完全不是一回事。搞懂了它们你才能处理好多域名调度、SSL证书匹配、日志分析、防盗链这些高频场景。这篇文章我就从变量来源讲起把三者的区别掰开揉碎再把常见的配置场景和排查思路结合起来争取你看完就能直接用不用再去翻文档。1. 三个变量的出身它们到底从哪来的先说结论$http_host、$host、$proxy_host的差异根源在于它们的“出处”完全不同。1.1 $http_host来自请求头的原始Host$http_host是一个通用变量。Nginx里所有以$http_开头的变量都是直接从HTTP请求头里取的$http_host就是请求头Host:这一行的原始值。比如客户端发起请求时带了这样一个头GET / HTTP/1.1 Host: www.example.com:8080那么Nginx解析出来的$http_host就是完整的www.example.com:8080端口号原样保留。如果客户端没有传Host头比如HTTP/1.0的某些老客户端或者用裸IP访问$http_host就会是空字符串。这里有个容易被忽略的点Nginx对请求头名称的大小写不敏感但变量值完全忠实于客户端发送的原始内容。也就是说客户端写host: example.com和Host: example.com$http_host拿到的都是example.com不会自己加端口也不会帮你把:80这种默认端口去掉。1.2 $hostNginx规范化处理后的主机名$host是Nginx核心模块内部定义的变量它不直接取请求头而是按照一套优先级规则来计算请求行request line里的主机名。比如请求行是GET http://www.example.com/index.html HTTP/1.1那$host就是www.example.com。如果请求行没有主机名就取请求头Host:里的主机名部分注意是去掉端口之后的部分。如果上面两条都为空就取当前server块里server_name指令配置的第一个名字。如果server_name也没配置默认取本机IP。换句话说$host是Nginx替你做了一层“容错处理”的变量。它永远返回一个不含端口的主机名优先级是请求行 Host头 server_name。这个设计初衷就是为了让Nginx在后端日志、内部跳转、URL重写这些场景里拿到的始终是“干净的域名”。举个例子GET / HTTP/1.1 Host: www.example.com:8080虽然$http_host返回www.example.com:8080但$host只会返回www.example.com。1.3 $proxy_host和proxy_pass绑定的目标主机$proxy_host和前两个完全不是一路人。它不是从外部请求里提取的而是Nginx在执行proxy_pass到上游服务器时打包出来的一个变量值是proxy_pass后面URL里的主机名和端口。例如location /api/ { proxy_pass http://10.0.0.5:8080/; }这个配置里$proxy_host的值就是10.0.0.5:8080。如果你写到Nginx日志里它永远是这个上游地址和客户端请求了什么域名没有任何关系。2. 核心区别一个表说清取值差异变量之间的区别光靠文字容易绕晕我直接做个对比表格。场景$http_host$host$proxy_host请求头 Host: www.abc.com 无端口www.abc.comwww.abc.com取决于proxy_pass请求头 Host: www.abc.com:8080www.abc.com:8080www.abc.com取决于proxy_pass请求行包含完整URL且带域名取Host头可能为空请求行主机名优先取决于proxy_passHTTP/1.0 无Host头空server_name的值取决于proxy_pass来源客户端请求头原样Nginx内部计算proxy_pass的URL是否带端口看客户端传什么一定不带看proxy_pass写什么注意最后一行$host一定不带端口即使在listen 443的HTTPS站点里也一样。很多人写日志发现$host没有端口以为配置错了其实这是正常行为。2.1 为什么不能随便互相替代因为它们来源不同所以用途必然不同。常见的错误是在proxy_set_header里把Host设成$proxy_host。为什么这样做会出问题因为$proxy_host本来就是proxy_pass里的上游地址比如10.0.0.5:8080你把请求头Host改成10.0.0.5:8080发给上游上游的虚拟主机看到的是IP和端口根本匹配不到你预期的server块直接给你返回host not defined或者默认站点。反过来有人做透传时用$http_host替代$host如果客户端恰好带了端口$http_host就带端口上游的server_name如果没监听这个端口也可能出现匹配问题。而$host因为去掉了端口匹配的宽容度更高。还有一点很关键$http_host是请求头里的原始值可能为空、可能带诡异的格式、甚至可能包含注入攻击常用的特殊字符。你在做安全和鉴权判断时用$host比用$http_host稳妥得多因为Nginx已经帮你做了规范化处理。3. 典型应用场景日志、转发、证书你会用到它们的每一处理解了区别接下来看实际的配置场景。这三个变量分布在Nginx配置的不同角落用途各有侧重。3.1 日志记录区分原始Host和规范化HostNginx的log_format是观察这三个变量最直接的地方。我常用的日志格式会在一条记录里同时打印$host和$http_hostlog_format main $remote_addr - [$time_local] $request host$host http_host$http_host status$status body_bytes_sent$body_bytes_sent $request_time; access_log /var/log/nginx/access.log main;这样配置有个很实用的好处你能直接看到真实请求里Host头带不带端口、有没有异常值。我见过很多灰度发布、域名切换出问题的情况最后都是靠对比$host和$http_host找出来的。比如线上有用户反馈“访问首页偶尔加载错误”日志里如果发现某些请求的$http_host为空、而$host正常那基本可以断定是客户端用了HTTP/1.0协议或者自己解析IP不走域名。这时候再配合access_log里的$request_line很快就能定位到源头。3.2 基于Host的路由与多域名转发多域名共用一台Nginx的场景极为常见。我有一次给一个客户做改造一台Nginx要同时代理a.example.com、b.example.com和c.test.com到三个不同的后端服务。最直观的做法是用server_name分开写server { listen 80; server_name a.example.com; location / { proxy_pass http://service-a:3000; } } server { listen 80; server_name b.example.com; location / { proxy_pass http://service-b:3001; } } server { listen 80; server_name c.test.com; location / { proxy_pass http://service-c:3002; } }这个配置不出错但你会看到server_name同时被用来做两件事一是Nginx选择哪个server块来处理请求二是$host变量的兜底来源。如果某个server块里你想做更细粒度的判断比如同一个server下根据Host不同走不同location就用if配合$host来分流server { listen 80; server_name _; location / { if ($host a.example.com) { proxy_pass http://service-a:3000; } if ($host b.example.com) { proxy_pass http://service-b:3001; } proxy_pass http://service-default:8080; } }这个写法在博客、工具类站点里很常见它比新建多个server块更紧凑。但要注意Nginx的if指令有一些历史遗留坑比如它内部的proxy_pass在部分版本上存在rewrite阶段执行顺序问题所以如果你在if里做了proxy_pass一定要用curl实测不同Host下的转发结果不要想当然。安全一点的替代方案是把判断写在server级别用map或者干脆用单独的server块一劳永逸。3.3 proxy_set_header里到底该填谁这是全文最容易搞混的地方。proxy_set_header Host的值不同直接决定了上游服务器接收到什么样的Host头。常见三种写法# 写法A透传客户端原始Host含端口 proxy_set_header Host $http_host; # 写法B透传Nginx规范化后的Host不含端口 proxy_set_header Host $host; # 写法C强制指定为固定域名 proxy_set_header Host api.example.com; # 写法D使用上游目标地址 proxy_set_header Host $proxy_host;逐一分析写法A把客户端请求头原封不动转发给上游。适合上游需要根据完整Host包括端口做判断的场景但与$host相比风险在于“原封不动”这四个字——客户端传了什么就是什么脏数据也会跟着进去。写法B最推荐的默认写法。Nginx帮你去了端口、做了容错上游拿到的永远是干净域名。写法C适合上游只认固定Host的场景。比如你代理到某个第三方API对方只接受特定的Host头此时用proxy_set_header Host api.example.com即可。写法D$proxy_host在proxy_set_header里几乎不要用。它取的是proxy_pass里的上游IP和端口比如10.0.0.5:8080这样转发后上游看到请求头是10.0.0.5:8080如果上游靠域名做虚拟主机路由必然出问题。我遇到过一个阿里云上部署的客户Nginx反代到内网某个服务内网服务要求Host必须是内网域名但公网域名不一样。这时候正确的做法是显式固定Host头而不是依赖任何一个变量proxy_set_header Host internal.service.local;注意如果后端服务又需要原始域名信息光设Host是远远不够的还得配合X-Forwarded-Hostproxy_set_header Host $host; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;这里X-Forwarded-Host故意用$http_host目的就是把客户端原始Host含端口交给后端后端如果需要做绝对URL生成、跳转逻辑拿到的信息就完整了。3.4 SSL证书匹配与多证书场景现在HTTPS普及率很高一个Nginx上绑多个证书的情况常见。最稳妥的方式仍然是不同域名用不同server块各自listen 443 ssl、各自配ssl_certificate。这种情况下不需要借助$host来做选择。但有些场景你确实需要在同一个server块里根据$host动态选证书比如使用OpenResty的ssl_certificate_by_lua。在这种脚本里$host是核心变量因为TLS握手发生在HTTP请求头解析之前$http_host都还没建立只有SNI里携带的域名可以被Lua读取最终也转化为$host供配置使用。举个简单的非脚本场景你有一个泛域名证书和一个精确域名证书Nginx原生不支持“按域名自动选证书”只能分开写server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /etc/nginx/certs/www.example.com.pem; ssl_certificate_key /etc/nginx/certs/www.example.com.key; } server { listen 443 ssl http2; server_name *.example.com; ssl_certificate /etc/nginx/certs/example.com.pem; ssl_certificate_key /etc/nginx/certs/example.com.key; }在这个结构里Nginx选择server块的依据其实就是“Host头/SNI解析出的主机名”原理和$host的取值逻辑一脉相承。很多“证书替换不生效”的问题排查到根本其实是访问时用的域名没落到正确的server块证书换对了文件但Nginx根本没读那份配置。3.5 防盗链和跨域判断防盗链、跨域白名单这类场景也经常要用到Host相关变量。比如只允许指定域名来访问的静态资源location ~* \.(jpg|png|gif)$ { valid_referers none blocked *.example.com; if ($invalid_referer) { return 403; } }这里用到的valid_referers判断依据是Referer头和Host变量无关。但如果要做基于Host的跨域限制就需要用$http_host或$host了location /api/ { set $cors_origin ; if ($http_host ~* ^(www\.)?example\.com$) { set $cors_origin $http_origin; } add_header Access-Control-Allow-Origin $cors_origin always; }这种写法判断的是请求方用哪个Host来访问$http_host和$host在这个场景下都可用。用$http_host更严格包含端口用$host更宽容忽略端口。从安全角度我倾向用$host因为你通常只想匹配域名本身不想让用户通过加一个诡异端口绕过限制。4. 实操中的高频坑从报错到配置失效一次说透不管变量理解得多么透彻实操中总会碰到各种意想不到的报错。我把这些年踩过、帮人排查过的和这三个变量相关的坑做一次集中记录。4.1 上游报错{result:host not defined}这个错误特别典型。上游返回host not defined通常就是后端靠Host头识别服务而Nginx转发时没把Host传对。我遇到过一个实际配置location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $proxy_host; }后端是Java服务内部用Host做了多租户路由结果$proxy_host算出来是127.0.0.1:8080后端一看这个Host不在租户列表里直接报host not defined。排查思路是先看Nginx日志里的$proxy_host到底是什么。如果你启用了自定义访问日志能看到每次请求的$proxy_host值很快就能确认是不是这个原因。修正方案很简单改成proxy_set_header Host $host;或者直接显式指定后端需要的那域名。4.2 https和证书相关的诡异问题“我明明替换了SSL证书但就是不生效”这个搜索热词背后十有八九是域名没有落到正确的server块。举例server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; } server { listen 443 ssl default_server; server_name _; ssl_certificate /etc/nginx/ssl/default.crt; ssl_certificate_key /etc/nginx/ssl/default.key; }如果你访问example.com不带www请求会走到default_server块看到的是默认证书。你把www.example.com那份证书换成了新文件但example.com一直走的是默认证书自然无效。这时候不要怀疑Nginx没加载新证书先检查你访问的域名落在了哪个server块。用curl就能看得明明白白curl -I https://example.com/ curl -I https://www.example.com/对比两份返回的证书信息立刻看出差异。4.3 端口丢失或保留引发的路由错误有次帮一个朋友排查他本地开发环境用的是localhost:8080访问NginxNginx反代到K8s里的服务结果后端一直返回404。日志里看到$http_host是localhost:8080$host是localhost。而K8s Ingress只认localhost不认带端口的localhost:8080。问题是他在proxy_set_header里写的是$http_host导致上游看到的是localhost:8080。将Host头改成$host后问题立刻消失。这类问题在迁移到容器编排平台之后特别常见。因为K8s、Swarm这类平台里的服务路由绝大多数只认Host域名不管端口。给你一个通用建议除非上游明确说要“原样保留端口”否则proxy_set_header Host一律用$host。这样你的Nginx在任何环境里转发都不会因为端口而翻车。4.4 空Host、HTTP/1.0请求、健康检查有时候你会发现日志里$http_host是空字符串但$host有值。这是正常现象前面说过HTTP/1.0老客户端不携带Host头。问题在于你如果用$http_host做转发上游就会收到一个不带Host的请求这在现代Web框架里很可能直接400。健康检查也有类似问题。云负载均衡的健康检查请求往往不带完整的Host头如果Nginx在location里用$host做了严格判断健康检查可能被拒导致实例被摘除。解决方法是把健康检查的请求单独指定Hostlocation /healthz { access_log off; return 200 ok; add_header Content-Type text/plain; }或者干脆不用Host做判断用固定路径做健康检查入口。4.5 使用return跳转时的坑$host在rewrite和return里也很常见比如HTTP跳HTTPSserver { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; }因为$http_host可能带端口如果没有特殊需求用$host做跳转更安全不会出现https://example.com:80/xxx这种离谱的跳转地址。反例return 301 https://$http_host$request_uri;如果客户端用http://example.com/abc访问$http_host是example.com无端口跳转没问题但如果客户端用http://example.com:8080/abc访问$http_host是example.com:8080跳转后就变成https://example.com:8080/abc浏览器会尝试往8080端口上走TLS握手绝对超时。5. 实操配置速查三种场景直接抄把上面的内容压缩成可直接落地的配置模板方便你按需取用。5.1 标准反向代理模板适用于绝大多数Web应用反代场景location / { proxy_pass http://backend-upstream; 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-Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; }这份配置里后端拿到的是干净域名$host同时通过X-Forwarded-Host拿到原始带端口Host做跳转、日志分析都不耽误。5.2 多域名多后端路由模板适合博客、工具站或灰度环境map $host $backend_pool { default web-default:8080; a.example.com web-a:8080; b.example.com web-b:8080; } server { listen 80; server_name _; location / { proxy_pass http://$backend_pool; proxy_set_header Host $host; } }用map比用if清晰得多而且没有rewrite阶段执行顺序的坑。$host作为map的匹配键天然帮你去掉了端口的干扰。5.3 日志调优模板如果你发现现有日志没法区分不同域名的流量加进Host变量就能解决log_format vhost $remote_addr - $http_host [$time_local] $request $status $body_bytes_sent host$host proxy_host$proxy_host upstream_addr$upstream_addr request_time$request_time;这样一条日志里同时有$http_host、$host和$proxy_host。做故障定位时一眼能看出客户端请求的原始Host、Nginx规范化后的Host、以及实际转发的上游地址三者对不上就是配置问题。6. 一些值得记住的经验最后分享几个我自己总结的习惯不算什么深奥原理但能帮你少走弯路。第一个经验是凡是需要“按域名做判断”的地方优先用$host不要用$http_host。原因很简单$host是Nginx官方推荐、规范化的变量取值稳定不带端口请求行和Host头都为空的时候还能拿server_name兜底。特别是在if、map、return这类场景里$host的表现永远比$http_host可控。第二个经验是每次改完proxy_set_header不要只看页面能不能打开要用curl实际验证后端拿到的是什么curl -H Host: a.example.com http://127.0.0.1:8080/ -v在后端日志里确认收到请求头里的Host是否符合预期。我的习惯是先在测试环境把日志里把$http_host和proxy_host加进去之后改配置只需要刷新页面并看日志输出就能快速确认。第三个经验是慎用$proxy_host做任何一件事。它属于“看起来有用但实际很容易出问题”的变量它的设计目的更多是让Nginx内部在处理上游连接时有地址可用而不是给你拿来写日志或配转发头的。我见过太多因为proxy_set_header Host $proxy_host导致的上游报错说句不客气的话生产环境里这一行几乎就是事故预兆。第四个经验是关于server_name的写法。有人说server_name可以写localhost、IP、下划线_确实都行但你要清楚$host的兜底逻辑依赖server_name。如果server_name _当客户端没传Host头时$host就会返回下划线_这个值在后端路由里很可能会被当成非法域名。生产环境如果一个server块承担了“兜底”职责建议给它一个明确的域名比如server_name default.example.com同时配合default_server使用这样日志和排查都会清晰很多。这三个变量本身不复杂复杂的是它们组合出来的一堆真实场景。把“来源、值、用途”对应好再配合日志看板Nginx的Host问题基本就能被你一眼看穿。