在 PVE LXC 部署 Beszel + Tailscale 私网监控平台

家里的 PVE 已经通过一个独立的 Tailscale 子网路由 LXC 接入 Tailnet。接下来要解决的问题,是把 PVE、虚拟机、LXC 和其他异地服务器的 CPU、内存、磁盘、网络及容器状态集中到一个长期在线的监控面板里。本文中的主机名、内网地址和容器编号均已改成占位符,避免公开家庭网络拓扑。

这次选择 Beszel 作为监控面板,并把 Beszel Hub 放进 PVE 的独立 Debian LXC。Tailscale 负责私网连接,Beszel 负责指标、历史图表和告警,整个面板不需要开放公网端口。

最终架构

Mac / 手机 / 其他管理端
        |
        | Tailscale
        v
PVE LXC:beszel
        |
        | Beszel Hub :8090
        |
        +-- PVE / Linux / macOS Agent
        +-- Docker / Podman 容器指标
        +-- CPU / 内存 / 磁盘 / 网络 / 告警

Beszel 由 Hub 和 Agent 两部分组成。Hub 保存配置和历史数据并提供 Web 界面,Agent 安装在每台需要监控的机器上。Hub 与 Agent 之间走 Tailscale 或家庭内网,不暴露到公网。

为什么使用独立 LXC

不建议把 Beszel 直接安装在 PVE 宿主机,也不建议和现有的 Tailscale 子网路由容器混装。独立 LXC 有几个好处:

  • PVE 升级、监控服务升级和子网路由故障互不影响。
  • 可以单独备份、快照和恢复 Beszel 数据。
  • Beszel Hub 是 Go 单文件程序,不需要为了运行它在 LXC 里再套一层 Docker。
  • 资源占用很低,1 vCPU、1 GB 内存和 8 GB 磁盘已经足够家庭环境使用。

部署前检查

先登录 PVE,确认容器 ID、存储、内存和网桥,避免与现有资源冲突:

ssh root@<pve-lan-ip>
pveversion
pct list
qm list
pvesm status
pvesh get /nodes/<pve-node>/status --output-format json
ip -brief address show vmbr0

本次检查到的实际环境如下:

  • PVE 版本:9.2.9。
  • 现有 LXC、VM 与计划使用的容器编号没有冲突。
  • local-lvm 的剩余空间足够创建 8 GB 根磁盘。
  • 可用内存满足 1 GB 内存和 512 MB Swap 的配置。
  • 网桥为 vmbr0,静态地址没有占用且已避开 DHCP 地址池。

LXC 参数

这次使用下面的配置:

VMID:       <unused-ctid>
Hostname:   beszel
OS:         Debian 13
CPU:        1 vCPU
Memory:     1024 MB
Swap:       512 MB
Disk:       8 GB / local-lvm
Network:    vmbr0
IPv4:       <beszel-lan-ip>/<cidr>
Gateway:    <lan-gateway>
Container:  unprivileged
On boot:    yes

地址使用前应检查 DHCP 静态租约和现有设备,不能只依赖一次 Ping 判断地址一定空闲。

下载 Debian 13 模板

先查看 PVE 当前提供的 Debian 模板:

pveam available --section system | grep debian-13

本次使用的是:

debian-13-standard_13.6-1_amd64.tar.zst

正常情况下直接下载到 local 存储:

pveam download local debian-13-standard_13.6-1_amd64.tar.zst

如果 PVE 到 Proxmox 下载源速度很慢,可以从另一台网络较快的机器下载同一个官方文件,再上传到 /var/lib/vz/template/cache/。上传完成后必须比较两端 SHA-256,校验通过后再创建容器。不要把未完成的临时文件当作模板使用。

创建 Beszel LXC

下面的命令在 PVE 宿主机执行。先创建非特权容器,再把 TUN 设备映射进去,供 Tailscale 使用:

pct create <unused-ctid> local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
  --hostname beszel \
  --ostype debian \
  --arch amd64 \
  --cores 1 \
  --memory 1024 \
  --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,firewall=1,ip=<beszel-lan-ip>/<cidr>,gw=<lan-gateway> \
  --nameserver <dns-server> \
  --unprivileged 1 \
  --onboot 1

pct set <unused-ctid> --dev0 path=/dev/net/tun
pct start <unused-ctid>

Debian 13 使用 systemd 257。本次第一次启动后,dev-mqueue.mountrun-lock.mounttmp.mount 失败,系统状态为 degraded。PVE 创建容器时也给出了 nesting 相关提示。停机后开启 nesting 再启动,失败单元清零:

pct stop <unused-ctid>
pct set <unused-ctid> --features nesting=1
pct start <unused-ctid>

pct exec <unused-ctid> -- systemctl is-system-running
pct exec <unused-ctid> -- systemctl --failed
pct exec <unused-ctid> -- ip -brief address
pct exec <unused-ctid> -- test -c /dev/net/tun

nesting=1 会放宽一部分容器隔离限制,不应把它当成所有 LXC 的默认配置;这里只对出现 systemd 挂载失败的 Beszel 容器启用。

下载慢时的模板处理

这次 PVE 直接连接下载源非常慢,从 Mac 经不稳定的 Tailscale 上传又留下了一个只有 1,044,480 字节的残缺文件。最终做法是删除这个明确确认过的残缺文件,让 PVE 从清华镜像下载到临时文件,校验 SHA-256 后再原子改名:

cd /var/lib/vz/template/cache
curl -fL \
  https://mirrors.tuna.tsinghua.edu.cn/proxmox/images/system/debian-13-standard_13.6-1_amd64.tar.zst \
  -o debian-13-standard_13.6-1_amd64.tar.zst.download

echo '1a5e43d088b430a1fca0531a2283ae2949ff0b40d383d0836d4a309e192b3244  debian-13-standard_13.6-1_amd64.tar.zst.download' \
  | sha256sum -c -

mv debian-13-standard_13.6-1_amd64.tar.zst.download \
  debian-13-standard_13.6-1_amd64.tar.zst

关键点不是使用哪一个镜像,而是始终先写入 .download,校验通过后才改成模板文件名。这样即使 SSH 或网络中断,PVE 也不会误用半个模板。

安装 Beszel Hub

进入容器,安装基础工具:

pct enter <unused-ctid>
export LC_ALL=C
apt update
apt install -y curl ca-certificates

Beszel 官方提供单文件安装脚本。比起直接执行 curl | sh,我习惯先下载并检查脚本,再运行:

curl -sL https://get.beszel.dev/hub -o /tmp/install-hub.sh
chmod +x /tmp/install-hub.sh
less /tmp/install-hub.sh

# GitHub 下载慢时使用安装器支持的镜像参数
/tmp/install-hub.sh -c https://ghfast.top/ --auto-update

本次安装得到 Beszel 0.18.8。程序位于 /opt/beszel/beszel,数据目录是 /opt/beszel/beszel_data,服务以专用的 beszel 用户运行。安装器还启用了每日更新定时器。

systemctl status beszel-hub --no-pager
systemctl status beszel-hub-update.timer --no-pager
/opt/beszel/beszel health --url http://127.0.0.1:8090
ss -lntp | grep 8090

实测健康检查返回 ok,局域网访问 http://<beszel-lan-ip>:8090/ 返回 HTTP 200。

第一次安装时还遇到一个容易忽略的问题:GitHub 下载很慢,旧的安装器进程没有及时退出。重新运行安装器前应先用 ps 核对进程命令和临时目录,只结束已确认属于旧安装器的 PID,并删除它对应的临时目录,不能用宽泛的 pkill 或通配符删除。

在 LXC 内安装 Tailscale

TUN 设备已经由 PVE 映射进容器。仍然先下载并检查官方脚本:

curl -fsSL https://tailscale.com/install.sh -o /tmp/install-tailscale.sh
less /tmp/install-tailscale.sh
sh /tmp/install-tailscale.sh

tailscale up --hostname=beszel --accept-dns=false

命令会输出一次性登录地址。在浏览器中确认目标设备名为 beszel,选择正确的 Tailnet,再点击 Connect。本次安装的 Tailscale 版本是 1.102.3

这里刻意没有增加 --ssh--advertise-routes--advertise-exit-node。这个 LXC 的职责只是运行监控面板,不承担远程 SSH、子网路由或出口节点职责,权限应保持最小化。

tailscale status --self
tailscale ip -4
tailscale debug prefs

最终核对结果是:设备已登录,主机名为 beszelRunSSH=falseAdvertiseRoutes=null,也没有配置 Exit Node。

从 Tailnet 访问 Beszel

如果 Tailnet 已启用 MagicDNS,可直接访问:

http://beszel:8090/

也可以使用容器分配到的 Tailscale IP:

http://<tailscale-ip>:8090/

本次从 Mac 验证:

tailscale ping beszel
curl -I http://<tailscale-ip>:8090/

Ping 成功,Hub 返回 HTTP 200。当前链路经 DERP 中继而不是直连,这不会影响管理面板使用,但延迟会高于同一局域网或 UDP 直连。

用 Tailscale Serve 提供 Tailnet HTTPS

直接访问 http://<tailscale-ip>:8090/ 已经只在 Tailnet 内可达,但浏览器仍会显示 HTTP。可以让 Tailscale Serve 为 Hub 提供自动 HTTPS 入口:

tailscale serve --bg 8090

首次运行可能会打开 Tailscale 管理确认页。本次确认页默认同时勾选了 Funnel,因此先取消 Funnel,只启用 HTTPS。Funnel 会把服务发布到公网,不符合本次私网监控的目标。

完成后,Beszel 可以通过下面的地址访问:

https://beszel.<tailnet-name>.ts.net/

最后检查 Serve 状态和 HTTPS 响应:

tailscale serve status
curl -I https://beszel.<tailnet-name>.ts.net/

实测 HTTPS 返回 HTTP 200,tailscale serve status 显示 tailnet only,请求被反向代理到 http://127.0.0.1:8090。这表示只有 Tailnet 成员能访问,没有启用 Funnel 公网发布。

首次登录与在 PVE 添加二进制 Agent

打开 Hub 后先创建管理员账号。密码不要写进命令、截图或博客。然后点击右上角的 Add System,在要监控的 PVE、Linux、macOS 或 Docker 主机上安装 Agent。

PVE 宿主机这里选择二进制,不选 Docker。二进制 Agent 直接作为 systemd 服务运行,能采集宿主机本身的 CPU、内存、磁盘和网络数据;为了装一个轻量 Agent 而额外在 PVE 宿主机引入 Docker 没有必要。Hub 继续留在独立 LXC,不与 Agent 混装。

先在 PVE 宿主机取得 Hub 的 SSH 公钥。下面的命令只从 Hub 私钥推导公钥,不读取或输出私钥内容:

HUB_PUBLIC_KEY="$(pct exec <beszel-ctid> -- \
  ssh-keygen -y -f /opt/beszel/beszel_data/id_ed25519)"

这次固定安装与 Hub 相同的 Beszel 0.18.8。官方安装脚本先下载到本地并核对本次审阅过的 SHA-256,再执行:

curl -fsSL \
  https://raw.githubusercontent.com/henrygd/beszel/v0.18.8/supplemental/scripts/install-agent.sh \
  -o /tmp/install-agent.sh

echo '1045acdf1fa29ff675a8d54218343241a987401e5572ae2beba557ec8c0463d4  /tmp/install-agent.sh' \
  | sha256sum -c -

chmod +x /tmp/install-agent.sh
/tmp/install-agent.sh \
  -k "$HUB_PUBLIC_KEY" \
  -p 45876 \
  -v 0.18.8 \
  --auto-update true \
  --mirror https://ghfast.top

unset HUB_PUBLIC_KEY

如果以后安装不同版本,不能继续照抄上面的 SHA-256;应重新下载、审阅并校验对应版本的脚本。

回到 Hub 的 Add System 页面,选择 Binary,然后填写:

  • Name:PVE,或自定义一个便于识别的名称。
  • Host / IP:<pve-lan-ip>,即 Beszel LXC 能直达的宿主机地址。
  • Port:45876
  • Public Key / Token:保留页面为当前系统生成的值,不把它们复制到公开文章。

点击 Add System 后,分别在 PVE 宿主机和 Hub 侧核验:

# PVE 宿主机
systemctl is-active beszel-agent
systemctl is-enabled beszel-agent
systemctl is-enabled beszel-agent-update.timer
ss -lntp | grep 45876
journalctl -u beszel-agent -n 50 --no-pager

# Beszel Hub LXC
systemctl is-active beszel-hub
journalctl -u beszel-hub -n 50 --no-pager

本次实测 Agent 为 active、enabled,自动更新 timer 已启用,端口 45876 正常监听。点击 Add System 后,Agent 日志出现 SSH connectedSSH connection established,证明 Hub 已经连上 PVE Agent。部分裸盘随后出现 no valid SMART data found,这表示对应磁盘没有返回可用 SMART 数据,不等同于 Agent 离线;基础 CPU、内存、磁盘容量和网络指标仍可正常采集。

继续添加其他 Tailnet Linux 节点

PVE 验证正常后,我又把一台家庭 Ubuntu、一台 Tailscale 路由 LXC 和一台 ARM64 云主机纳入监控。三台机器都已经加入同一个 Tailnet,因此 Agent 不需要监听所有网卡,只绑定各自的 Tailscale 地址:

<node-tailscale-ip>:45876

有 root 或免密 sudo 的 Debian、Ubuntu 节点继续使用前文的二进制安装脚本,只需要把 -p 改成对应的 Tailscale 地址。安装器会自动选择 AMD64 或 ARM64 程序:

/tmp/install-agent.sh \
  -k "$HUB_PUBLIC_KEY" \
  -p <node-tailscale-ip>:45876 \
  -v 0.18.8 \
  --auto-update true \
  --mirror https://ghfast.top

家庭 Ubuntu 的 SSH 账号需要交互式 sudo 密码,但账号本身已经在 docker 组中,因此这台机器改用官方 Docker Agent。Docker Hub 当时无法路由,改从 Beszel 官方 GHCR 地址拉取同一版本:

docker pull ghcr.io/henrygd/beszel/beszel-agent:0.18.8

docker run -d \
  --name beszel-agent \
  --network host \
  --restart unless-stopped \
  -v beszel-agent-data:/var/lib/beszel-agent \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -e "KEY=$HUB_PUBLIC_KEY" \
  -e LISTEN=<node-tailscale-ip>:45876 \
  ghcr.io/henrygd/beszel/beszel-agent:0.18.8

--network host 用于采集宿主机网络接口统计;docker.sock 只读挂载用于采集 Docker 容器指标。Agent 只配置 SSH 公钥时,日志可能提示没有设置 HUB_URL,这是因为未启用 Agent 主动连接 Hub 的 WebSocket 模式,不影响 Hub 主动连接 Agent 的 SSH 模式。

每台机器安装后,都从 Hub LXC 主动连接其 Tailscale 地址验证 SSH 握手。三台返回的版本横幅均为:

SSH-2.0-beszel_0.18.8

最后在 Hub 中分别添加系统,Host / IP 使用对应的 Tailscale 地址,端口统一填写 45876。真实节点名称、地址、公钥和 Token 不应出现在公开文章中。

一次真实的 Tailscale 路由故障恢复

部署过程中,原有 Tailscale 路由容器的数据面卡住,Mac 和我都无法通过原路径连接家庭内网。此时不能在同一条已断的隧道上反复 SSH。我的备用路径是一台仍在线的异地云主机:

  1. 在备用节点临时接受现有子网路由。
  2. 以它作为 SSH Jump Host 进入 PVE 的局域网地址。
  3. systemd-run 延迟重启路由容器,让重启命令不依赖当前 SSH 会话继续存活。
  4. 连接恢复后,立即关闭备用节点的 accept-routes
# 备用节点
sudo tailscale set --accept-routes=true

# 从管理端经备用节点进入 PVE
ssh -J <jump-host> root@<pve-lan-ip>

# 在 PVE 上延迟 3 秒重启路由容器
systemd-run \
  --unit=restart-router-lxc \
  --on-active=3s \
  /usr/sbin/pct reboot <router-ctid>

# 路由恢复并验证后,在备用节点撤销临时设置
sudo tailscale set --accept-routes=false

恢复后应验证路由容器为 running、tailscaled 为 active、局域网子网路由重新可达,并再次检查备用节点的 RouteAll=false。这类临时改路由的操作要有明确的回收步骤,否则故障恢复本身会留下新的流量路径。

最终验收清单

  • Beszel LXC 为 running,开机自启,系统没有 failed unit。
  • /dev/net/tun 在非特权 LXC 内可用。
  • beszel-hub 与自动更新 timer 已启用。
  • Hub 本机健康检查为 ok,LAN、Tailscale HTTP 与 Tailnet HTTPS 访问均为 HTTP 200。
  • Tailscale Serve 状态为 tailnet only,没有启用 Funnel 公网发布。
  • Tailscale 设备名为 beszel,没有开启 SSH、子网路由或 Exit Node。
  • PVE 上的 beszel-agent 与更新 timer 已启用,Hub 日志确认 SSH 连接建立。
  • 其他 Tailnet 节点的 Agent 只监听各自的 Tailscale 地址,Hub 侧 SSH 握手版本一致。
  • PVE 没有新增公网端口映射。
  • 定期备份 /opt/beszel/beszel_data,并把 Beszel LXC 纳入 PVE 备份计划。

参考资料

发表评论