Nezha 面板部署实战

Azure VPS 上部署 Nezha 监控面板的完整实战记录,涵盖 Caddy 反代、Cloudflare 代理、agent 连接、WAF 配置、SSH 加固等关键环节。

#type / howto #status / growing #tech / ops #tech / monitoring

[!info] related notes

Nezha 面板部署实战

目标

在一台 Azure VPS(香港)上部署 Nezha 监控面板,通过 Caddy 反代 + Cloudflare 代理统一入口,并接入远端 VPS 的 agent 进行监控。

架构总览

浏览器 ──► Cloudflare (橙云) ──► Azure NSG ──► Caddy (:443) ──► Nezha Dashboard (127.0.0.1:8008)

远端 Agent ──► /etc/hosts 直连 ──► Azure NSG ──► Caddy (:443) ── gRPC ───┘

关键设计决策:所有服务(Sub-Store、Nezha 面板)统一走 Caddy 反代,Azure NSG 只开放 22/80/443,不额外暴露服务端口。


服务器端配置

Caddy 反代配置

nezha.bakersean.top {
    @websocket {
        header Connection *Upgrade*
        header Upgrade websocket
    }

    handle @websocket {
        reverse_proxy 127.0.0.1:8008
    }

    encode zstd gzip

    reverse_proxy 127.0.0.1:8008 {
        transport http {
            versions h2c 1.1
        }
    }
}

要点

  • WebSocket 路径必须在 encode 指令之前单独处理,否则压缩中间件会干扰 WebSocket 升级握手
  • transport http { versions h2c 1.1 } 确保 Caddy 向后端发送 HTTP/1.1(Nezha 的 gRPC 和 HTTP 混用需要这个)
  • 非 WebSocket 请求正常走 gzip/zstd 压缩

Nezha Dashboard 核心配置

listen_host: 127.0.0.1
listen_port: 8008
dashboard_host: nezha.bakersean.top
install_host: nezha.bakersean.top:443
tls: true
web_real_ip_header: CF-Connecting-IP    # Cloudflare 代理时必须用这个
agent_real_ip_header: X-Forwarded-For
agent_secret_key: "<强随机密钥>"
site_name: Baker Nezha

要点

  • web_real_ip_header 必须设为 CF-Connecting-IP(走 Cloudflare 代理时),否则面板内置 WAF 会误封访客
  • install_host 用于生成 agent 安装命令,填 域名:443
  • tls: true 表示 agent 用 TLS 连接

UFW 防火墙

服务器 INPUT 默认策略为 deny,只放行必要来源:

# SSH 全开
sudo ufw allow 22/tcp

# 80/443 只允许 Cloudflare IP 段
sudo ufw allow from 173.245.48.0/20 proto tcp to any port 80,443
sudo ufw allow from 103.21.244.0/22 proto tcp to any port 80,443
# ... (其他 Cloudflare IP 段同理)

# 远端 agent 直连 IP 单独放行
sudo ufw allow from <韩国VPS公网IP> proto tcp to any port 443 comment "Nezha agent direct"

远端 Agent 配置

安装

# 下载 agent binary (Debian/Ubuntu)
sudo apt-get install -y unzip
cd /tmp
curl -sL -o nezha-agent.zip "https://github.com/nezhahq/agent/releases/download/v2.2.2/nezha-agent_linux_amd64.zip"
unzip nezha-agent.zip -d /tmp/nezha-agent-extract
sudo mkdir -p /opt/nezha/agent
sudo cp /tmp/nezha-agent-extract/nezha-agent /opt/nezha/agent/
sudo chmod +x /opt/nezha/agent/nezha-agent

配置文件 /opt/nezha/agent/config.yaml

server: nezha.bakersean.top:443
client_secret: <与 dashboard 的 agent_secret_key 一致>
uuid: <每台 agent 独立的 UUID>
tls: true
debug: false
disable_command_execute: true
disable_force_update: true

/etc/hosts 绕过 Cloudflare(关键)

52.229.227.206 nezha.bakersean.top

为什么需要这条:Cloudflare 免费套餐对 gRPC/HTTP2 长连接有超时限制,agent 走 Cloudflare 代理会被断开。加了 hosts 后 agent 直连服务器 IP,但配置仍用域名(确保 TLS SNI 正确匹配 Caddy 证书)。

Systemd 服务

[Unit]
Description=Nezha Agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=nezha
Group=nezha
WorkingDirectory=/opt/nezha/agent
ExecStart=/opt/nezha/agent/nezha-agent -c /opt/nezha/agent/config.yaml
Restart=always
RestartSec=5
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

踩坑与排障

1. WebSocket 连不上(面板一直 “WebSocket 连接中”)

原因:Caddy 的 encode zstd gzip 指令压缩了 WebSocket 升级响应,导致浏览器无法完成握手。

修复:在 encode 之前用 @websocket matcher + handle 单独处理 WebSocket 请求。

2. 面板 403 + 500 错误(“Unexpected token ’<’”)

原因:重置 admin 密码后 token_version 递增,浏览器缓存的旧 JWT 失效。所有 API 返回 403,WebSocket 也失败,前端拿到 HTML 错误页当 JSON 解析就报 500。

修复:清除 nezha.bakersean.top 的浏览器 Cookie,重新登录。

3. 切回 Cloudflare 橙云后全站不可达(ERR_CONNECTION_CLOSED)

原因:UFW 只允许 Cloudflare IP 段访问 80/443。切 DNS only(灰云)时流量直连服务器,来源 IP 不在 Cloudflare 段,被 DROP。

修复:把 DNS 切回 Proxied(橙云),让流量经过 Cloudflare 代理。

4. Nezha WAF 误封(“Blocked by nezha WAF”)

原因web_real_ip_header 配置为 X-Forwarded-For,走 Cloudflare 代理时该头的值是 Cloudflare 节点 IP,WAF 识别错 IP 后触发封禁。

修复:改 web_real_ip_headerCF-Connecting-IP,并清空 nz_waf 表。

sudo sed -i 's/^web_real_ip_header:.*/web_real_ip_header: CF-Connecting-IP/' /opt/nezha/dashboard/data/config.yaml
sudo -u nezha sqlite3 /opt/nezha/dashboard/data/sqlite.db "DELETE FROM nz_waf;"
sudo systemctl restart nezha-dashboard

5. Agent 切橙云后掉线

原因:Cloudflare 免费套餐不支持持久 gRPC 长连接,agent 的 gRPC stream 被超时断开。

修复:agent 端 /etc/hosts52.229.227.206 nezha.bakersean.top 直连服务器,绕过 Cloudflare。同时在 UFW 放行 agent 的公网 IP。

6. SSH 被暴力破解挤断

原因:公网 VPS 每分钟收到大量 root 登录尝试,占满 MaxStartups(默认 10 个未认证连接槽位),导致新 SSH 连接在 kex_exchange_identification 阶段被丢弃。

修复

# 关闭 root 登录和密码认证
sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

管理命令速查

操作命令
查看 dashboard 状态sudo systemctl status nezha-dashboard --no-pager
查看 agent 连接sudo ss -tnp | grep nezha-agent
重置 admin 密码见下方脚本
查看 WAF 拦截记录sudo -u nezha sqlite3 /opt/nezha/dashboard/data/sqlite.db 'SELECT * FROM nz_waf;'
清空 WAF 记录sudo -u nezha sqlite3 /opt/nezha/dashboard/data/sqlite.db 'DELETE FROM nz_waf;'
查看已注册 agentsudo -u nezha sqlite3 /opt/nezha/dashboard/data/sqlite.db 'SELECT id, name, uuid FROM servers;'

重置 admin 密码脚本

NEW_PASS="你的新密码"
HASHED=$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'${NEW_PASS}', bcrypt.gensalt(rounds=10)).decode())")
sudo -u nezha sqlite3 /opt/nezha/dashboard/data/sqlite.db \
  "UPDATE users SET password='$HASHED', reject_password=0, token_version=token_version+1 WHERE username='admin';"
sudo systemctl restart nezha-dashboard

[!warning] 注意 重置密码会递增 token_version,导致所有已登录的浏览器 session 失效,需要重新登录。


经验总结

  1. Cloudflare 橙云 vs 灰云的选择:浏览器访问走橙云(安全和 CDN),agent 连接必须绕过 Cloudflare(直连 IP + hosts 条目),两者不冲突。
  2. Caddy 反代 WebSocketencode 指令会影响 WebSocket 升级,必须用 matcher 单独处理。
  3. Nezha 内置 WAF:依赖 web_real_ip_header 识别访客 IP,Cloudflare 代理下必须用 CF-Connecting-IP
  4. 公网 VPS SSH 加固是刚需:关闭 root 登录 + 禁用密码认证 + fail2ban,否则暴力破解会耗尽连接资源。
  5. 同机部署 Nezha 的局限:dashboard 和被监控机器在同一台 VPS 时,整机宕机会导致监控一起失联,适合做服务级监控,不适合做”机器死没死”的最终裁判。
创建于 2026/6/18 更新于 2026/6/18