Nezha 面板部署实战
Azure VPS 上部署 Nezha 监控面板的完整实战记录,涵盖 Caddy 反代、Cloudflare 代理、agent 连接、WAF 配置、SSH 加固等关键环节。
[!info] related notes
- 相关资源: Cloudflare, Caddy, UFW, [[fail2ban|Fail2ban]]
- 相关协议: gRPC, HTTPS/TLS, [[tls-sni|TLS SNI]]
- 相关实战: Home Server Setup, Cloudflare 自定义域名邮件
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 安装命令,填域名:443tls: 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_header 为 CF-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/hosts 加 52.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;' |
| 查看已注册 agent | sudo -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 失效,需要重新登录。
经验总结
- Cloudflare 橙云 vs 灰云的选择:浏览器访问走橙云(安全和 CDN),agent 连接必须绕过 Cloudflare(直连 IP + hosts 条目),两者不冲突。
- Caddy 反代 WebSocket:
encode指令会影响 WebSocket 升级,必须用 matcher 单独处理。 - Nezha 内置 WAF:依赖
web_real_ip_header识别访客 IP,Cloudflare 代理下必须用CF-Connecting-IP。 - 公网 VPS SSH 加固是刚需:关闭 root 登录 + 禁用密码认证 + fail2ban,否则暴力破解会耗尽连接资源。
- 同机部署 Nezha 的局限:dashboard 和被监控机器在同一台 VPS 时,整机宕机会导致监控一起失联,适合做服务级监控,不适合做”机器死没死”的最终裁判。