在 NAS 上通过 Docker 部署了一系列服务后,使用 Nginx(或 OpenResty)做统一的反向代理是常规操作。然而,在接入 Home Assistant(HA) 时,很多人都会迎头撞上一个极其顽固的错误:网页端无法加载,控制台疯狂报错 400 Bad Request

更让人崩溃的是,当你为了解决 HA 的问题修改了 Nginx 配置后,可能会发现 NAS 上的其他服务(比如为知笔记 Wiznote)又双叒叕连不上了!本文将复盘这一系列"按下葫芦浮起瓢"的踩坑过程,并给出能完美兼容所有服务的终极 Nginx 配置。


症状描述

  1. 通过内网 IP + 端口访问 HA 一切正常。
  2. 通过 Nginx 反向代理域名(如 https://nas.example.com:10461)访问 HA 时,页面卡在登录或白屏。
  3. 按下 F12 打开浏览器控制台,会看到以下报错:
Manifest fetch from .../manifest.json failed, code 400
WebSocket connection to 'wss://.../api/websocket' failed

两大核心根因

这次 400 错误并不是单纯的一个问题,而是两个安全机制叠加产生的"连环套":

根因一:HA 的代理白名单机制

Home Assistant 对安全性要求极高。默认情况下,它会拒绝所有来自未知反向代理服务器的请求,以防止 IP 欺骗。如果 Nginx 部署在 Docker 中,或者走的是 IPv6 回环地址(::1),HA 识别到的源 IP 就会发生变化。只要这个 IP 不在 HA 的白名单里,一律无情 400 拦截。

根因二:Nginx 的 WebSocket 全局污染

HA 极度依赖 WebSocket 进行实时通信。为了让代理支持 WebSocket,很多人在 Nginx 全局 http 块中直接加上:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

这就是灾难的开始。 这会导致 Nginx 把所有发往 HA 的普通 HTTP 请求(如获取图片或 manifest.json)都强行伪装成 WebSocket 升级请求,HA 底层的 Web 服务器发现格式不对,同样报 400。

此外,这种全局配置还会波及同服务器上的其他服务,比如为知笔记(Wiznote)。为知笔记依赖 HTTP 长连接(Keep-Alive)来加载数据,收到强制的 upgradeclose 指令后,直接罢工断连。

附加根因:端口透传丢失

如果使用了 proxy_set_header Host $host;,会丢失用户请求的原始端口号。对于内部依赖端口号进行数据交互的服务(如为知笔记网页端),这会导致界面能打开但数据无限转圈。


终极解决方案

第一步:设置 HA 的"全内网"白名单

最稳妥的方法是把所有安全的私有网段(涵盖局域网、Docker 网桥、IPv6 本地地址)全部加进 HA 的配置文件。

打开 HA 映射目录下的 configuration.yaml,添加或修改以下内容:

http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 127.0.0.0/8       # 本机 IPv4
    - ::1               # 本机 IPv6 (极其重要,很多本地代理会走这个)
    - 10.0.0.0/8        # A 类私有网络
    - 172.16.0.0/12     # Docker 默认的虚拟网络网段
    - 192.168.0.0/16    # C 类私有网络(涵盖大部分家用路由器网段)
    - fe80::/10         # 本地链路 IPv6

⚠️ 修改完成后,必须完全重启 Home Assistant 容器,配置才能生效:

docker restart homeassistant

第二步:配置动态 WebSocket + 端口透传(Nginx)

核心思路是动态判断客户端是否真的在请求 WebSocket。同时,使用 $http_host 完美透传原始端口,确保其他服务不被干扰。

打开 Nginx(或 OpenResty)配置文件 nginx.conf:

http {
    # ... 其他基础配置 ...

    # 1. map 块:动态识别 WebSocket 请求
    # 收到 Upgrade 头才升级,否则保持普通长连接(拯救为知笔记等服务)
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    # 2. 全局代理参数优化
    # 使用 $http_host 完美透传域名和端口
    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;

    # 3. 统一开启动态 WebSocket 支持
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # --- 代理 Home Assistant ---
    server {
        listen 10461 ssl;
        server_name nas.yourdomain.com;
        location / {
            proxy_pass http://192.168.31.3:8123;
            # 无需再重复写 proxy_set_header,完美继承全局变量
        }
    }

    # --- 代理为知笔记(完全不受干扰,顺滑加载) ---
    server {
        listen 10449 ssl;
        server_name nas.yourdomain.com;
        location / {
            proxy_pass http://192.168.31.3:9000;
        }
    }
}

第三步:重载生效

sudo nginx -t
sudo nginx -s reload

总结

问题 解决方案
HA 400 + Docker/IPv6 源 IP 变化 configuration.yaml 加全网段 trusted_proxies
全局 upgrade 污染其他服务 map $http_upgrade $connection_upgrade 动态判断
$host 丢失原始端口 改用 $http_host 透传
改完 HA 不生效 docker restart homeassistant 重启容器

在复杂的 NAS 容器网络中遇到代理报 400 Bad Request 时,查阅目标服务的日志永远是第一步。改用 $http_hostmap 动态升级方案后,不仅仅是 Home Assistant 和 Wiznote,以后在这台服务器上部署任何奇奇怪怪的 Web 服务,这套基础网络架构都能稳稳支撑!