搞过Nginx反向代理的同学基本都绕不开这三个变量$http_host、$host、$proxy_host。表面上看都是“Host”但实际含义差了十万八千里用错了轻则日志记录不准重则转发到上游后业务直接异常比如重定向地址不对、WebSocket握手失败、IP白名单失效、路径拼接错误等。我见过不少资深的运维同事在这上面栽过跟头而且这类问题排查起来特别隐蔽因为Nginx本身不报错业务方反馈的报错信息又五花八门绕来绕去最后才发现是Host头处理出了问题。这篇文章就把这三兄弟彻底讲透。我会直接对比它们各自的来源、取值逻辑、适用场景再给出几个踩坑典型和完整的配置示例。不管你是刚入门Nginx的小白还是已经在生产环境里维护反代服务的老手这篇文章都能帮你省下不少排查时间。1. 内容整体设计与思路拆解1.1 为什么很多人分不清这三个变量先说结论这三个变量确实都跟“主机名”相关但它们的来源不同、作用阶段不同、服务对象不同。$http_host来自客户端HTTP请求头里的Host字段理论上最接近用户原始意图$host是Nginx自己解析后的主机名规则更加“规范化”$proxy_host则是Nginx作为代理时proxy_pass指令里指定的目标主机地址。换句话说前两个变量描述的是“客户端想访问谁”最后一个描述的是“Nginx打算转发给谁”。如果你在server块里用proxy_pass http://backend这种方式做反代Nginx默认会把$proxy_host作为上游请求的Host头传过去而这个值通常是后端服务器的内网IP或者upstream组名不是用户请求里的域名。这就是很多反代场景下业务出问题的根源。1.2 搞懂变量才能真正看懂Nginx的转发逻辑Nginx的核心能力就是“接收请求、处理请求、转发请求”。接收阶段Nginx需要知道客户端访问的是哪个域名所以会解析请求头里的Host字段得到$http_host再结合server_name匹配规则确定进入哪个server块。处理阶段Nginx会通过一系列变量辅助条件判断、重写URL、记录日志。转发阶段Nginx根据proxy_pass拼装上游请求同时决定把什么值放到上游请求的Host头里这里就会用到$proxy_host。这三个阶段环环相扣任何一环理解不到位配置就容易出错。比如你只在server块里配置了listen 80 default_server没有对应server_name那么$host和$http_host在特定请求下表现会完全不一样。再比如你配置了国际化域名的Punycode转换$host会返回解码后的Unicode值而$http_host保留的可能是转换前的ASCII形式。这些细枝末节平时用不到但关键时刻就是排查思路的分水岭。1.3 本文的阅读路径建议我建议你先通读第一部分的变量对比搞清楚三者的定义和边界再去看第二部分的使用场景。如果你已经遇到了具体问题可以直接跳到第四部分的排查实录对照速查表可能更快定位问题。第三部分是一整套配置示例适合在了解原理后照着搭一个反向代理环境把各种Host处理方式都跑一遍彻底形成体感。2. 核心变量细节解析与实操要点2.1 $http_host客户端原始请求头里的Host$http_host直接取自客户端HTTP请求头里的Host字段它的值完全由客户端决定Nginx不会做任何改写。这里有个非常关键的点HTTP/1.1协议规定客户端必须携带Host头所以正常情况下$http_host不会为空。但如果你用HTTP/1.0协议发起请求或者某些畸形客户端不发送Host头那$http_host就是一个空字符串。另外注意$http_host里包含端口号。假设用户访问https://example.com:8443/login$http_host的值就是example.com:8443端口号原封不动地保留。这一点在写日志模板或者做请求头转发时一定要留意不然很可能把端口号丢失导致后端重定向地址没有端口跳转后链接不可用。还有一个很容易被忽略的细节如果客户端请求头里存在多个相同名称的Host字段Nginx会拿第一个值作为$http_host。这种请求比较少见但一旦出现日志里记录的值可能和预期不一致不要觉得是Nginx有bug这是它的固有行为。2.2 $hostNginx内部解析后的规范化主机名$host的取值优先级是这样首先是请求行里的host参数如果没有再看请求头里的Host字段如果连请求头里都没有就用当前server块配置的server_name。这三个来源逐级降级保证了$host在绝大多数场景下都有值。和$http_host最大的区别是$host不包含端口号。它会自动把请求头Host字段里的端口部分去掉只保留主机名。如果你在server_name里配置的是Punycode编码的国际化域名$host会返回解码后的Unicode字符串比如客户端请求xn--fsqu00a.xn--0zwm56d$host可能返回例子.测试而$http_host返回的是原始的ASCII编码形式。所以在做域名比较、拼接跳转地址、做缓存key这类逻辑时优先使用$host避免因格式不一致引发歧义。这里要专门强调一下$host的值虽然经过Nginx规范化处理但它并不等于请求一定匹配到了某个server_name。即使没有匹配任何server块Nginx采用了默认server$host依然会拿请求头里的Host字段来解析。也就是说$host反映的是“请求里携带的主机名”经过规范化后的值而不代表“Nginx配置里声明的某个server_name”。2.3 $proxy_hostproxy_pass的目标主机地址$proxy_host是Nginx在HTTP代理模块里定义的变量只有当配置里使用了proxy_pass指令且Nginx实际执行了转发动作时这个变量才会有值。它的取值来自proxy_pass里指定的目标主机包括协议、主机名或IP、端口但不包括URI部分。一个典型场景你配置了proxy_pass http://backend_server;而backend_server是upstream组的名字那么$proxy_host就是backend_server。注意这里是upstream组的名称字符串而不是组里某个具体后端节点的IP地址。Nginx在处理upstream时本身也不知道应该用哪个IP作为Host头所以只能拿组名字符串去填充。如果你写的是proxy_pass http://192.168.1.10:8080;那$proxy_host就是192.168.1.10:8080。这意味着什么意味着默认情况下Nginx转发给上游的Host头是上游服务器的IP和端口而不是用户访问的域名。很多后端应用依赖Host头来识别域名、生成绝对链接、做多租户隔离默认行为直接导致业务错乱。2.4 三者对比速查变量名来源是否含端口典型值示例说明$http_host客户端请求头Host字段含example.com:8080原样取请求头不做任何处理$host请求行/请求头/server_name不含example.comNginx规范化后的主机名$proxy_hostproxy_pass指令目标含192.168.1.10:8080或backend_server仅在执行代理转发时有值表格看下来核心记忆点就是$http_host是“用户嘴里说的”$host是“Nginx记下来的”$proxy_host是“Nginx最终递出去的”。理清这个链条后面所有配置都顺了。3. 实操过程与核心环节实现3.1 动手搭建测试环境我在本地用Docker快速搭了一个测试环境用来验证这三个变量在不同配置下的输出值。Docker的好处是隔离干净随便折腾也不会污染宿主环境而且可以同时起Nginx和后端服务完整模拟真实转发的链路。如果你也想手动验证可以直接参考下面的命令。先创建一个测试目录写一个后端服务这里直接用Python起一个简单的HTTP服务器专门打印收到的请求头。Python的http.server虽然轻量但能非常直观地展示上游收到什么东西对排查这类问题很有帮助。# server.py from http.server import HTTPServer, BaseHTTPRequestHandler import json class Handler(BaseHTTPRequestHandler): def do_GET(self): headers {k: v for k, v in self.headers.items()} body json.dumps({path: self.path, headers: headers}, indent2).encode(utf-8) self.send_response(200) self.send_header(Content-Type, application/json) self.end_headers() self.wfile.write(body) def log_message(self, format, *args): print(f[backend] {self.address_string()} - {format % args}) HTTPServer((0.0.0.0, 8080), Handler).serve_forever()这个后端服务监听8080端口收到请求后会把路径和所有请求头以JSON格式返回。通过观察返回的Host头就能知道Nginx转发时到底带了什么值。3.2 场景一保持客户端Host头不变这是最常见的需求尤其是后端要根据用户访问的域名来做路由或者生成业务链接时。配置非常简单用proxy_set_header强制把上游请求的Host头设置成客户端原始值。server { listen 80; server_name example.com; location / { proxy_pass http://backend:8080; proxy_set_header Host $http_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; } }用curl -H Host: example.com:8888 http://127.0.0.1/测试后端返回的JSON里Host头就是example.com:8888和客户端请求完全一致。这个场景下使用$http_host就能完整保留端口信息。但要注意如果客户端没有带端口那$http_host就没有端口号这和$host的表现是一样的。3.3 场景二隐藏内部端口只保留域名有时候客户端访问的是http://example.com:8000但后端应用需要看到的Host头是example.com不带端口。这时候继续用$http_host就会把8000端口透传过去后端如果拿这个端口做重定向拼接就会把带端口的地址暴露给用户用户体验很差而且有时候内网端口直接暴露出来还有安全隐患。这种场景用$host最合适它已经把端口剥离干净了。location / { proxy_pass http://backend:8080; proxy_set_header Host $host; }再配合curl -H Host: example.com:8000 http://127.0.0.1/测试后端拿到的Host头是example.com端口被Nginx去掉。仔细体会这个区别$http_host保留的是用户原始输入$host返回的是规范化后的主机名两者处理的出发点完全不同。3.4 场景三动态upstream和SSL后端证书校验理解$proxy_host的动态语义特别重要尤其是当你配置了upstream集群或域名式后端时。先看一个用upstream的例子upstream backend_servers { server 192.168.1.10:8080 weight3; server 192.168.1.11:8080 weight1; } server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/certs/example.crt; ssl_certificate_key /etc/nginx/certs/example.key; location / { proxy_pass http://backend_servers; proxy_set_header Host $proxy_host; } }实际运行时传给上游的Host头要么是192.168.1.10:8080要么是192.168.1.11:8080取决于负载均衡算法选中的节点。这种配置在需求上其实非常少见但如果你不显式设置proxy_set_header HostNginx默认就是拿$proxy_host去填充的。这就是很多人在做反代时后端拿到IP而不是域名的根本原因。如果上游是HTTPS还会涉及SNI证书校验的问题。当后端服务器上配了多个虚拟主机用同一个IP和443端口对外提供服务时Nginx握手时需要通过SNI携带Host名称让后端选择正确的证书。这时候就不能把$proxy_host传给后端的SSL握手了因为$proxy_host是upstream组名或者IP后端根本无法根据它选择证书。你需要手动强制设置SNIlocation / { proxy_pass https://backend_servers; proxy_set_header Host $host; proxy_ssl_server_name on; proxy_ssl_name $host; }proxy_ssl_name指定SNI里携带的服务器名称这里用$host把用户请求的域名传过去后端才能正确匹配证书。这个配置细节我见过不止一个团队在处理多域名HTTPS反代时踩坑。3.5 场景四日志记录里的变量差异访问日志里的Host字段很多人的第一反应是用$http_host。这么做有个隐患如果客户端直接访问Nginx的IP地址没有带合法的Host头$http_host就是空的日志里就会留下一个空格排障的时候完全看不出用户访问了哪个站点。如果启用的是$hostNginx会用默认server的server_name来兜底日志至少是完整的。log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for host$host http_host$http_host proxy_host$proxy_host;这段日志模板把三个变量全部记录下来排查问题的时候可以直接对比差异非常直观。生产环境里我建议同时记录$host和$http_host因为前者用于分析用户实际访问的站点后者用于排查某些特殊客户端是否发送了非法或异常的Host头。这两种信息侧重点不同组合使用能更快定位问题。3.5 一个完整的综合配置参考下面是一段生产环境级别的综合配置覆盖了静态资源、动态接口和WebSocket三种常见代理场景。注意其中针对不同场景选择了不同的Host变量不是所有location都千篇一律使用$host。upstream api_servers { server 10.0.0.11:8080; server 10.0.0.12:8080; keepalive 32; } server { listen 80; server_name www.example.com example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name www.example.com example.com; ssl_certificate /etc/nginx/ssl/example.com.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; # 静态资源配置直接用本地文件系统响应不需要转发 location /static/ { alias /data/www/static/; expires 7d; access_log off; } # 动态接口代理 location /api/ { proxy_pass http://api_servers; 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_connect_timeout 5s; proxy_read_timeout 10s; proxy_send_timeout 10s; } # WebSocket代理 location /ws/ { proxy_pass http://api_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }逻辑线很清晰静态资源不经过代理直接由Nginx本地处理动态接口代理到后端Java服务Host头用不带端口的$hostWebSocket代理需要额外放行Upgrade和Connection头Host头同样用$host保证业务方拿到的域名干净统一。这套配置虽然简单但基本覆盖了普通业务的绝大部分需求你可以根据自己的实际拓扑做调整。3.6 用curl验证所有变量的真实值配置完成后验证环节不能省。用curl直接构造不同Host头的请求观察后端返回的头部信息就能确实掌握三个变量的实际区别。下面是我本地测试的一组结果你可以对照着看。先验证默认转发行为故意不对Host做任何设置curl -H Host: www.example.com:8000 http://127.0.0.1/api/hello后端返回内容里的Host字段如果是backend:8080说明使用的是默认的$proxy_host。再改成显式传递$hostproxy_set_header Host $host;重新执行同样命令后端收到的Host字段就是www.example.com端口已经消失。最后试验$http_host后端收到的是www.example.com:8000端口原样保留。4. 常见问题与排查技巧实录4.1 后台上游收到的Host一直是内网IP这个问题出现的频率极高典型的反馈是“我明明配置了proxy_pass指向域名为什么后端日志里看到的是内网IP”如果你没有显式设置proxy_set_header HostNginx默认用$proxy_host填充上游请求的Host头而$proxy_host的值是proxy_pass目标地址里的主机名。如果目标是IP或upstream组名后端收到的自然就是IP或组名。解决办法很简单在location里显式声明一行就行proxy_set_header Host $host;这里我强烈建议所有使用proxy_pass的场景都必须显式设置proxy_set_header Host哪怕你只是想保持默认行为也写出来方便后来人理解你的意图。不声明的配置等于把默认行为藏在暗处后期接手的人很容易误解。4.2 HTTPS跳转后端口丢失用户访问https://example.com:8443/loginNginx做了301跳转到https://example.com/login端口直接消失用户点开就404。这类问题多半是Nginx配置了HTTP到HTTPS的强制跳转或者后端应用根据Host头生成了重定向地址。排查的时候先看跳转发生在哪一层是Nginx的return 301还是后端应用返回的Location头。如果是Nginx层跳转配置里用了$host做域名拼接端口自然没了。你需要的是$http_host因为它保留客户端原始端口。改法如下return 301 https://$http_host$request_uri;修改之后用户访问https://example.com:8443/login时$http_host的值是example.com:8443拼接出来的跳转地址就会带上8443端口。但这里还有一个隐藏坑如果客户端访问的是80端口$http_host就不带端口这个方案依然正确。所以核心逻辑是跳转时想保留什么就选什么变量端口是变量选择的分水岭。4.3 WebSocket握手失败或频繁掉线WebSocket场景下握手阶段客户端会通过Upgrade: websocket和Connection: Upgrade两个请求头来发起协议升级。Nginx默认情况下会忽略客户端传入的Connection头导致握手失败。如果你配置的WebSocket反代里把Host也设错了后端可能直接拒绝连接。标准代理配置长这样location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; }proxy_http_version 1.1必须显式声明因为HTTP/1.0不支持Upgrade机制。Connection upgrade覆盖了Nginx默认的Connection清理逻辑让WebSocket帧能顺利透传。Host头用$host或$http_host都可以根据后端需求决定但不要漏掉否则某些严格校验Host的应用会直接拒绝握手请求。4.4 多域名共用一个Nginx如何正确区分路由如果一个Nginx通过server_name承载多个域名的转发千万不要在location里写死proxy_set_header Host backend.example.com这种固定值。一旦两个域名共用同一个后端后端就无法区分用户到底访问的是哪个站点导致“所有域名都返回同一个主页”这类问题。正确做法是让后端信任Nginx传递的请求域名也就是用$host或者$http_host。如果后端是多租户架构需要精确知道用户访问的原始域名和端口用$http_host接收原始值如果只需要域名本身做路由用$host即可还能顺带规避端口带来的麻烦proxy_set_header Host $http_host;这个配置尤其适用于需要按Host头区分租户的SaaS系统比如用户访问tenant1.example.com和tenant2.example.com后端拿到的Host头就是对应的租户域名从而正确渲染各自的业务数据。4.5 调试技巧速查问题现象可能原因排查方向后端收到的是IP而不是域名未设置proxy_set_header Host默认用了$proxy_host检查location里是否声明了Host变量重定向后端口丢失使用了$host拼跳转地址改用$http_hostWebSocket握手失败未配置Upgrade/Connection头检查proxy_http_version和相关头设置日志里Host字段是空的客户端未携带Host头$http_host为空改用$host或$server_name兜底多租户域名错乱Host头被写死或未透传显式传给后端$http_host除了上述场景我再推荐一个调试辅助手段。在Nginx的server块里临时加一个location返回JSON格式的变量快照排查时非常高效location /nginx_vars { default_type application/json; return 200 {\host\:\$host\,\http_host\:\$http_host\,\proxy_host\:\$proxy_host\,\server_name\:\$server_name\,\remote_addr\:\$remote_addr\}; }上线前记得删除这个location不然等于公开暴露了Nginx的转发细节有一定安全隐患。我在本地测试环境经常保留它生产环境从来不开这是底线。5. 按需选择不要让默认行为替你做决定最后再分享一个我在实际运维中的习惯。每次配置新的反向代理时我都会先自问三个问题用户访问的域名是什么客户端原始端口是否需要保留后端服务依赖于Host头的哪个部分做业务判断回答完这三个问题再决定用$host还是$http_host还是$proxy_host基本不会出大乱子。$host适合大多数透传场景因为它剥离了端口格式规范对后端友好。$http_host适合需要完整保留客户端访问信息的场景尤其是涉及端口和多租户路由时。$proxy_host则主要用于那些明确需要把上游节点信息暴露给后端的极少数场景绝大多数业务用不到。排障的时候把这三个变量的值同时打到日志里上下游一对比问题原因往往立刻浮出水面。默认行为只是Nginx替你做的兜底决策不代表业务的最佳选择关键位置一定要显式声明这是反代配置里最重要的一条纪律。