哪吒面板多域名探针部署教程(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 |
架构总览

| 域名 | 用途 | 能看面板? | 能连探针? | 拦截方式 |
|---|---|---|---|---|
| 主面板域名 | 看数据 | ✅ | ❌ | 按原项目配置 |
| CF 探针域名 | CF 线路探针 | ❌ | ✅ | CF 隧道只放行 gRPC,浏览器 404 |
| 直连探针域名 | 直连线路探针 | ❌ | ✅ | NPM 假端口 9999,浏览器 502 |
前置条件
- 已按照 原项目文档 完成主面板域名的部署,面板可正常访问
- VPS 上已部署 Nginx Proxy Manager 容器(端口映射:
80:80,443:443,管理端口:81) - 一个不在 CF 托管的直连域名(DNS A 记录直接指向 VPS 公网 IP)
第一部分:直连探针域名
⚠️ 直连域名没有 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 | 故意填不存在的端口! |
🔴 绝对不能填真实的面板端口(如 8080)! 这里填假地址是拦截浏览器访问的核心机制:
- 浏览器访问时路径为
/,Nginx 找不到高级规则匹配,就会把请求送到这个假地址,直接 502 报错 - 探针访问时路径为
/proto.NezhaService/,命中高级规则,走绿色通道进入真实的 8008 端口
SSL 选项卡
| 设置项 | 值 |
|---|---|
| SSL Certificate | Request a new SSL Certificate |
| Force SSL | ✅ 开启 |
| HTTP/2 Support | ✅ 开启(gRPC 强依赖 HTTP/2) |
| Use a DNS Challenge | ✅ 开启(推荐,一劳永逸免续签) |
| DNS Provider | Cloudflare |
| Credentials | dns_cloudflare_api_token = 你的CF_API_Token |
💡 强烈建议使用 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;
}
🔴 grpc_set_header Host panel.mydomain.com; 这行是命根子! 哪吒服务端只认最初配置的主域名,不管你从哪个域名连进来,都必须伪装成主域名才能通过认证。
1.3 工作原理

第二部分:CF 探针域名
ℹ️ 由于 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 | 真实面板端口 |
ℹ️ 这里可以填真实地址,因为 CF 隧道在海外节点已经把浏览器流量拦截了(只放行 proto.NezhaService 路径),普通浏览器根本到不了 NPM 这一层。
SSL 选项卡
| 设置项 | 值 |
|---|---|
| SSL Certificate | Request a new SSL Certificate |
| Force SSL | ✅ 开启 |
| HTTP/2 Support | ✅ 开启 |
| Use a DNS Challenge | ✅ 开启 |
| 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 |
🔴 这里是最大的坑!必须填 172.17.0.1:443,绝对不能填 localhost:443! 因为 cloudflared 跑在 Docker 容器内部,localhost 指向的是容器自己,不是宿主机。172.17.0.1 是 Docker 网桥的网关地址,从容器内部看就是宿主机。
⚠️ 只添加 proto.NezhaService 这一条路径规则!不要添加留空(匹配所有)的兜底规则! 这样当浏览器试图访问这个域名时,CF 隧道找不到匹配的路径,会直接返回 404,实现完美拦截。
额外设置(Additional application settings)
展开 TLS 设置:
| 设置项 | 值 | 说明 |
|---|---|---|
| No TLS Verify | ✅ 开启 | 跳过证书验证 |
| HTTP2 connection | ✅ 开启 | gRPC 强依赖 HTTP/2 |
| Origin Server Name | agent-cf.mydomain2.com | 极度关键!见下方说明 |
🔴 Origin Server Name 必须填域名! 如果不填,cloudflared 在 TLS 握手时会用 IP 地址 172.17.0.1 作为 SNI。NPM 根据 SNI 来路由流量,认不出 IP 地址就会把流量扔进默认垃圾桶,返回 502。
展开 HTTP 设置:
| 设置项 | 值 | 说明 |
|---|---|---|
| HTTP Host Header | 留空!不要填! | Host 伪装在 NPM 里做,不在这里做 |
⚠️ 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 开关已开启。
🔴 如果不开启 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"
🔴 永远不要在 NPM 的高级设置里使用 if 语句做全局拦截!
# ❌ 错误示范!千万不要这样写!
if ($http_content_type !~ "application/grpc") {
return 444;
}
Nginx 的 if 语句会破坏配置继承机制,导致 underscores_in_headers on; 等指令失效,密码 Header 被吞掉,所有域名的探针全部报 Unauthenticated。
✅ 正确的拦截方式:用假端口(9999)实现路由级别的物理隔离,无需 if。
坑 2:不同域名必须分开配置
🔴 在 NPM 里,不同用途的域名必须各自新建一条 Proxy Host!
NPM 的一条 Proxy Host 里,Details 的转发目标对所有填入的域名一视同仁。如果你把"要看面板的主域名"和"要拦截面板的探针域名"塞进同一条规则,它们就会共享同一个转发目标,拦截必然失败。
坑 3:argo-nezha 容器内的 localhost ≠ 宿主机
🔴 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 必须填
🔴 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 不能打架
🔴 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(临时放行):
- 在 CF 隧道临时加一条留空路径规则,指向
http://172.17.0.1:80 - NPM 申请证书成功后,立刻删掉这条临时规则
解决方案 B(一劳永逸,推荐):
使用 DNS Challenge 方式申请证书,完全不需要 80 端口。
- 去 https://dash.cloudflare.com/profile/api-tokens 创建一个 "编辑区域 DNS" 的 Token
- 在 NPM SSL 设置里开启
Use a DNS Challenge,选 Cloudflare,填入 Token - 证书自动续签,永不过期
快速排查清单
当探针连不上时,按以下顺序排查:
| 步骤 | 检查项 | 操作 |
|---|---|---|
| 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' |