Nezha 面板三域名职责分离部署方案 (NPM + Cloudflare)

哪吒面板多域名探针部署教程(argo-nezha + NPM + CF 隧道)

本教程基于 Argo-Nezha-Service-Container 项目,在其基础上扩展多探针域名,配合 Nginx Proxy Manager (NPM) 实现职责分离架构。

主面板域名的部署请参考原项目文档,本教程不再赘述,仅聚焦于"如何额外添加直连探针域名和 CF 探针域名"。


域名说明

本教程中使用以下示例域名,请替换为你自己的真实域名:

示例域名 角色 说明
panel.mydomain.com 主面板域名 托管在 CF,按原项目文档配置即可
agent-cf.mydomain2.com CF 探针域名 托管在 CF,专门给探针用,有 CDN 加速
agent-direct.mydomain3.com 直连探针域名 不在 CF 托管,DNS 直接指向 VPS IP

架构总览

域名 用途 能看面板? 能连探针? 拦截方式
主面板域名 看数据 :white_check_mark: :cross_mark: 按原项目配置
CF 探针域名 CF 线路探针 :cross_mark: :white_check_mark: CF 隧道只放行 gRPC,浏览器 404
直连探针域名 直连线路探针 :cross_mark: :white_check_mark: NPM 假端口 9999,浏览器 502

前置条件

  • 已按照 原项目文档 完成主面板域名的部署,面板可正常访问
  • VPS 上已部署 Nginx Proxy Manager 容器(端口映射:80:80, 443:443, 管理端口:81)
  • 一个不在 CF 托管的直连域名(DNS A 记录直接指向 VPS 公网 IP)

第一部分:直连探针域名

:warning: 直连域名没有 CF 保护,必须在 NPM 里实现拦截,防止面板被外人偷窥。

1.1 DNS 设置

在你的 DNS 服务商(非 Cloudflare)那里,添加一条 A 记录:

  • 主机名: 你的直连子域名(如 agent-direct.mydomain3.com)
  • 记录值: 你 VPS 的公网 IP
  • 代理状态: 无(直连,不走 CF CDN)

1.2 NPM 配置

在 NPM 后台 → Add Proxy Host(新建一条,不要和其他域名混在一起!)

Details 选项卡(核心:用假端口实现拦截)

设置项 值 说明
Domain Names agent-direct.mydomain3.com 你的直连域名
Scheme http
Forward Hostname / IP 127.0.0.1 故意填假地址!
Forward Port 9999 故意填不存在的端口!

:red_circle: 绝对不能填真实的面板端口(如 8080)! 这里填假地址是拦截浏览器访问的核心机制:

  • 浏览器访问时路径为 /,Nginx 找不到高级规则匹配,就会把请求送到这个假地址,直接 502 报错
  • 探针访问时路径为 /proto.NezhaService/,命中高级规则,走绿色通道进入真实的 8008 端口

SSL 选项卡

设置项 值
SSL Certificate Request a new SSL Certificate
Force SSL :white_check_mark: 开启
HTTP/2 Support :white_check_mark: 开启(gRPC 强依赖 HTTP/2)
Use a DNS Challenge :white_check_mark: 开启(推荐,一劳永逸免续签)
DNS Provider Cloudflare
Credentials dns_cloudflare_api_token = 你的CF_API_Token

:light_bulb: 强烈建议使用 DNS Challenge! 这样证书到期后会全自动续签,不需要开放 80 端口。
获取 API Token 的地址:https://dash.cloudflare.com/profile/api-tokens
创建令牌时选择 编辑区域 DNS (Edit zone DNS) 模板,选中对应域名即可。

Advanced 选项卡(给探针开绿色通道)

# 允许带下划线的 Header 透明传输(哪吒认证必备)
underscores_in_headers on;

# 仅针对探针通讯路径进行 gRPC 转发
location /proto.NezhaService/ {
    grpc_read_timeout 300s;
    grpc_send_timeout 300s;
    grpc_socket_keepalive on;
    
    # 透传认证信息
    grpc_set_header password $http_password;
    grpc_set_header Authorization $http_authorization;
    
    # 伪装主域名的 Host 头(哪吒服务端只认主域名)
    grpc_set_header Host panel.mydomain.com;
    
    # 转发到真实的哪吒内部通讯端口
    grpc_pass grpc://172.17.0.1:8008;
}

:red_circle: grpc_set_header Host panel.mydomain.com; 这行是命根子! 哪吒服务端只认最初配置的主域名,不管你从哪个域名连进来,都必须伪装成主域名才能通过认证。

1.3 工作原理


第二部分:CF 探针域名

:information_source: 由于 argo-nezha 的 cloudflared 跑在容器内部,localhost:443 会连到容器自己的内部代理。为了走 NPM 的 gRPC 规则,必须把转发目标改成 172.17.0.1:443(指向宿主机的 NPM)。

2.1 NPM 配置

在 NPM 后台 → Add Proxy Host(同样必须单独新建!)

Details 选项卡

设置项 值 说明
Domain Names agent-cf.mydomain2.com CF 探针域名
Scheme http
Forward Hostname / IP 172.17.0.1 真实面板地址
Forward Port 8080 真实面板端口

:information_source: 这里可以填真实地址,因为 CF 隧道在海外节点已经把浏览器流量拦截了(只放行 proto.NezhaService 路径),普通浏览器根本到不了 NPM 这一层。

SSL 选项卡

设置项 值
SSL Certificate Request a new SSL Certificate
Force SSL :white_check_mark: 开启
HTTP/2 Support :white_check_mark: 开启
Use a DNS Challenge :white_check_mark: 开启
DNS Provider Cloudflare
Credentials dns_cloudflare_api_token = 你的CF_API_Token

Advanced 选项卡

underscores_in_headers on;

location /proto.NezhaService/ {
    grpc_read_timeout 300s;
    grpc_send_timeout 300s;
    grpc_socket_keepalive on;
    
    grpc_set_header password $http_password;
    grpc_set_header Authorization $http_authorization;
    
    # 同样必须伪装主域名
    grpc_set_header Host panel.mydomain.com;
    
    grpc_pass grpc://172.17.0.1:8008;
}

2.2 CF 隧道配置

在 Cloudflare Zero Trust → Tunnels → 你的隧道 → Public Hostname 中,为 CF 探针域名添加规则:

子域名 域 路径 服务类型 URL
agent-cf mydomain2.com proto.NezhaService HTTPS 172.17.0.1:443

:red_circle: 这里是最大的坑!必须填 172.17.0.1:443,绝对不能填 localhost:443! 因为 cloudflared 跑在 Docker 容器内部,localhost 指向的是容器自己,不是宿主机。172.17.0.1 是 Docker 网桥的网关地址,从容器内部看就是宿主机。

:warning: 只添加 proto.NezhaService 这一条路径规则!不要添加留空(匹配所有)的兜底规则! 这样当浏览器试图访问这个域名时,CF 隧道找不到匹配的路径,会直接返回 404,实现完美拦截。

额外设置(Additional application settings)

展开 TLS 设置:

设置项 值 说明
No TLS Verify :white_check_mark: 开启 跳过证书验证
HTTP2 connection :white_check_mark: 开启 gRPC 强依赖 HTTP/2
Origin Server Name agent-cf.mydomain2.com 极度关键!见下方说明

:red_circle: Origin Server Name 必须填域名! 如果不填,cloudflared 在 TLS 握手时会用 IP 地址 172.17.0.1 作为 SNI。NPM 根据 SNI 来路由流量,认不出 IP 地址就会把流量扔进默认垃圾桶,返回 502。

展开 HTTP 设置:

设置项 值 说明
HTTP Host Header 留空!不要填! Host 伪装在 NPM 里做,不在这里做

:warning: HTTP Host Header 必须留空! 如果你在这里填了主域名(如 panel.mydomain.com),会导致 TLS 的 SNI(agent-cf.mydomain2.com)和 HTTP 的 Host 头(panel.mydomain.com)不一致。NPM 检测到 SNI 和 Host 头的矛盾,会直接返回 421 Misdirected Request。Host 伪装的工作交给 NPM 高级设置里的 grpc_set_header Host panel.mydomain.com; 来做。

2.3 CF 域名面板设置

在 Cloudflare 域名管理后台 → 网络 (Network) → 确保 gRPC 开关已开启。

:red_circle: 如果不开启 gRPC,CF 会把探针的 gRPC 流量当成普通 HTTP 处理,直接返回 403 Forbidden。


第三部分:探针客户端启动命令

在需要监控的机器上,使用以下命令启动探针:

通过 CF 探针域名连接(有 CDN 加速):

./nezha-agent -s agent-cf.mydomain2.com:443 --tls --client-secret 你的密钥

通过直连探针域名连接(无 CDN,延迟更低):

./nezha-agent -s agent-direct.mydomain3.com:443 --tls --client-secret 你的密钥

踩坑记录与排查指南

坑 1:Nginx 的 “If is Evil”

:red_circle: 永远不要在 NPM 的高级设置里使用 if 语句做全局拦截!

# ❌ 错误示范!千万不要这样写!
if ($http_content_type !~ "application/grpc") {
    return 444;
}

Nginx 的 if 语句会破坏配置继承机制,导致 underscores_in_headers on; 等指令失效,密码 Header 被吞掉,所有域名的探针全部报 Unauthenticated。

:white_check_mark: 正确的拦截方式:用假端口(9999)实现路由级别的物理隔离,无需 if。


坑 2:不同域名必须分开配置

:red_circle: 在 NPM 里,不同用途的域名必须各自新建一条 Proxy Host!

NPM 的一条 Proxy Host 里,Details 的转发目标对所有填入的域名一视同仁。如果你把"要看面板的主域名"和"要拦截面板的探针域名"塞进同一条规则,它们就会共享同一个转发目标,拦截必然失败。


坑 3:argo-nezha 容器内的 localhost ≠ 宿主机

:red_circle: argo-nezha 镜像的 cloudflared 跑在容器内部!localhost 是容器自己,不是宿主机!

如果你在 CF 隧道里填 localhost:443,流量会被送到容器内部的代理,永远不会到达宿主机的 NPM。必须改成 172.17.0.1:443(Docker 网桥网关地址 = 宿主机)。

排查方法: 检查 NPM 对应域名的访问日志是否为空。如果是空文件(0 字节),说明流量根本没进来:

# 查看 NPM 生成的配置文件
docker exec NPM容器名 sh -c 'cat /data/nginx/proxy_host/*.conf'

# 查看对应域名的访问日志
docker exec NPM容器名 sh -c 'tail -20 /data/logs/proxy-host-N_access.log'

坑 4:Origin Server Name 必须填

:red_circle: CF 隧道 TLS 设置里的 Origin Server Name 必须填写目标域名!

当 cloudflared 连接 172.17.0.1:443 时,如果不指定 Origin Server Name,TLS 握手的 SNI 会是 IP 地址 172.17.0.1。NPM 根据 SNI 路由流量,认不出 IP 就扔进默认垃圾桶,返回 502。


坑 5:HTTP Host Header 和 Origin Server Name 不能打架

:red_circle: CF 隧道的 HTTP Host Header 必须留空!

如果 HTTP Host Header 填了主域名(用于伪装),而 Origin Server Name 填了探针域名,NPM 会检测到 SNI 和 Host 头不一致,返回 421 Misdirected Request。

Host 伪装应该在 NPM 的 Advanced 里用 grpc_set_header Host 来做,让 CF 隧道只负责老老实实地把流量送进 NPM 的正确大门。


坑 6:SSL 证书申请失败(Internal Error)

如果 NPM 申请 Let’s Encrypt 证书时报 Internal Error,通常是因为 80 端口验证不通。

解决方案 A(临时放行):

  1. 在 CF 隧道临时加一条留空路径规则,指向 http://172.17.0.1:80
  2. NPM 申请证书成功后,立刻删掉这条临时规则

解决方案 B(一劳永逸,推荐):
使用 DNS Challenge 方式申请证书,完全不需要 80 端口。

  1. 去 https://dash.cloudflare.com/profile/api-tokens 创建一个 “编辑区域 DNS” 的 Token
  2. 在 NPM SSL 设置里开启 Use a DNS Challenge,选 Cloudflare,填入 Token
  3. 证书自动续签,永不过期

快速排查清单

当探针连不上时,按以下顺序排查:

步骤 检查项 操作
1 NPM 日志是否有流量 docker exec NPM容器名 sh -c 'tail -20 /data/logs/proxy-host-N_access.log'
2 日志为空 流量没到 NPM → 检查 CF 隧道目标是不是 172.17.0.1:443
3 日志有记录但 502 检查 CF 隧道 TLS 的 Origin Server Name 是否填了域名
4 日志有记录但 421 检查 CF 隧道 HTTP 设置的 Host Header 是否留空
5 报 Unauthenticated 检查 Advanced 里是否有 grpc_set_header Host 主面板域名;
6 报 403 Forbidden 检查 CF 域名面板 Network 里 gRPC 开关是否开启
7 查看实际配置 docker exec NPM容器名 sh -c 'cat /data/nginx/proxy_host/*.conf'
1 个赞

学习一下

6A大佬厉害啊

学习学习