部署与运维
生产形态是一个二进制 + 一个配置目录 + 一个 systemd unit。没有数据库服务、没有消息
队列、没有运行时依赖——除非你要 BLE,那多一个 libdbus-1-3。
文中所有实测数字都出自同一台机器:N150 小主机,Proxmox VE 9.2.3,容器是 Debian 13 的 非特权 LXC(256M / 2 core / 4G)。
一、选哪个二进制
Section titled “一、选哪个二进制”CI 每次构建都产出两个 artifact,直接下载就行,不必自己 cargo build:
| artifact | 含 BLE | 链接方式 |
|---|---|---|
rha-x86_64-gnu-full |
是 | 动态,目标机要有 libdbus-1-3 |
rha-x86_64-musl-nobluetooth |
否 | 完全静态,扔进空目录就能跑 |
判据只有一条:要不要接小米的 BLE 设备——温湿度计、体重秤这类走 mibeacon 的东西。要就拿 gnu 变体,不要就拿 musl。
原因是 BLE 那条路依赖 bluer → libdbus-1,那是个 C 库,进不了完全静态的 musl 产物。
所以「单文件零依赖」和「能收 BLE 广播」在这里是二选一,不是配置开关。
其余功能两个变体完全一样。HomeKit 桥走的是 IP,依赖链 (mDNS 那一串)全是纯 Rust,静态链接没有障碍,musl 变体里也有。
二、放哪,怎么跑起来
Section titled “二、放哪,怎么跑起来”三个约定位置:
/usr/local/bin/rha 二进制/etc/rha/ 配置目录(rha.toml、devices.toml、automations/、bridges.toml、spec.d/)/var/lib/rha/history.db 历史库--config /etc/rha 指的就是那个配置目录,形状和快速开始里的
examples/quickstart 完全一样。
历史库要显式挪出配置目录。 [storage].path 缺省是
<配置目录>/history.db,也就是 /etc/rha/history.db——能写,但一个每分钟都在追加写的
SQLite 文件不该躺在配置目录里(备份配置时会顺手带走几百兆,回滚配置时又容易误伤):
[storage]path = "/var/lib/rha/history.db"systemd unit
Section titled “systemd unit”packaging/rha.service 可以直接用。几个不明显的地方:
Type=notify + WatchdogSec=30s。 daemon 就绪后给 systemd 发 READY=1,之后每
15 秒(半周期)喂一次狗。主循环卡死时喂狗停止,systemd 把它重启掉——这是「进程还在、
但已经不干活了」的唯一兜底。真机上用 SIGSTOP 模拟过:Watchdog timeout (limit 30s)! →
SIGABRT → 重启。
/etc/rha 必须可写。 unit 开了 ProtectSystem=strict(整个 /etc 变只读),然后
又用 ReadWritePaths 把 /etc/rha 单独放回来。三个理由,只要占一条就绕不开:
- MCP 的
upsert_rule要往<配置目录>/automations/落规则文件(先写.toml.tmp再 rename); - storage 的缺省路径就在配置目录下(按上面挪走以后这条不成立);
- 面板文件存在
<配置目录>/dashboards/。
只有第 1 条是真正无法回避的。完全不需要让 agent 写规则的话可以去掉这一项,upsert_rule
会因为写盘失败返回错误,不会静默丢弃。
BLE 部署下不要再往严里调。 unit 里刻意没有 PrivateUsers / RestrictAddressFamilies
这类选项——它们会切断和 bluetoothd 的 D-Bus 通信(D-Bus 是 Linux 上进程之间互相喊话的
总线,蓝牙栈就挂在上面)。纯 musl 无 BLE 的部署可以自行加严。
三、LXC 容器部署
Section titled “三、LXC 容器部署”主力形态是 Proxmox VE 上的一个非特权 Debian LXC。packaging/lxc-setup.sh 在 PVE
宿主上以 root 跑:
./lxc-setup.sh --ctid 140 --ip 192.168.52.40/21 --gw 192.168.50.1 --dry-run # 先看一遍./lxc-setup.sh --ctid 140 --ip 192.168.52.40/21 --gw 192.168.50.1它替你做的:建容器(unprivileged + onboot)→ 对齐时区 → 探 DNS → 装
ca-certificates 和 libdbus-1-3 → 建 rha 服务账号 → 建
/var/lib/rha、/etc/rha、/etc/rha/{automations,bridges,dashboards},以及一个空的
/etc/rha/rha.env 占位文件。
二进制、unit 和配置要你自己传,脚本尾部把命令逐条列出来了(pct push 那几条)。
脚本不幂等:容器已经存在时会在 pct create 那一步直接中断。重跑前先
pct destroy <ctid>,或者跳到脚本末尾的手动步骤。容器内部的步骤本身都是幂等的。
两个坑,脚本已经处理,换环境时仍要留意
Section titled “两个坑,脚本已经处理,换环境时仍要留意”DNS 会黑洞。 不传 --nameserver 时 PVE 把宿主的 /etc/resolv.conf 抄进容器。
宿主要是装了 Tailscale,那份文件指向 MagicDNS 的 100.100.100.100——容器不在 tailnet
里,这个地址不可达,于是 apt-get update 无限挂起且一个字都不报。脚本缺省把
nameserver 退回网关(家用 LAN 上通常就是 DNS),并在装包前探测一次;探测外面套了
timeout,因为 getent 自己不超时,不套的话这个「检查」本身就会卡死。
时区默认是 UTC。 规则里的 time = "HH:MM..HH:MM" 条件和 cron 触发都按容器本地
时间判定,不对齐宿主会整体偏几个小时(实测偏 8 小时,所有定时规则在错误的时刻触发,
而日志看起来一切正常)。脚本会同步宿主时区。
内存给 256M 是留了余量的起点,目标形态是 128M:首次部署时 apt 的峰值容易顶到
128M,跑稳之后 pct set 140 --memory 128 收下来。
BLE 要 D-Bus 直通
Section titled “BLE 要 D-Bus 直通”只有用 mibeacon 才需要这一节,也是整套部署里最容易 卡住的一步。不用 BLE 就部署 musl 变体,下面全都不需要。
思路是:bluetoothd 跑在 PVE 宿主上独占射频,容器只当 D-Bus 客户端。这样容器不需要
任何 /dev 设备权限,保持 unprivileged,只需要能连上宿主的系统总线。下面这套在 N150
上实测跑通,五步缺一不可。
-
宿主装并启用 BlueZ(PVE 默认只有内核驱动,不装 BlueZ)
Terminal window apt-get install -y bluez && systemctl enable --now bluetoothbluetoothctl show # 确认 Powered: yes -
把宿主的 D-Bus socket 挂进容器,挂到独立路径——不要覆盖容器自己的
/var/run/dbus/system_bus_socket,那会和容器内的dbus-daemon打架。先在容器里install -d /var/lib/rha-dbus,然后/etc/pve/lxc/<CTID>.conf追加:lxc.mount.entry: /var/run/dbus/system_bus_socket var/lib/rha-dbus/system_bus_socket none bind,create=file 0 0 -
让 rha 的 uid 在容器内外一致。 宿主的 dbus 只接受
EXTERNAL认证(靠SO_PEERCRED读对端 uid),而 libdbus 总是显式断言自己的 uid。非特权容器里的 uid 999 在宿主看来是 100999,对不上就直接REJECTED EXTERNAL。往/etc/subuid加root:999:1、/etc/subgid加root:991:1,然后配置里追加:lxc.idmap: u 0 100000 999lxc.idmap: u 999 999 1lxc.idmap: u 1000 101000 64536lxc.idmap: g 0 100000 991lxc.idmap: g 991 991 1lxc.idmap: g 992 100992 64544改 idmap 之后磁盘上原有文件的属主要一起改,否则 rha 连自己的配置都读不到:
Terminal window pct stop <CTID> && pct mount <CTID>find /var/lib/lxc/<CTID>/rootfs -uid 100999 -exec chown 999 {} +find /var/lib/lxc/<CTID>/rootfs -gid 100991 -exec chgrp 991 {} +pct unmount <CTID> && pct start <CTID> -
宿主上也得存在 uid 999 这个账号。
Terminal window groupadd --system --gid 991 rhauseradd --system --uid 999 --gid 991 --no-create-home --shell /usr/sbin/nologin rha -
让 rha 去连宿主的总线(容器内
/etc/systemd/system/rha.service.d/dbus.conf):[Service]Environment=DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/lib/rha-dbus/system_bus_socket# ProtectSystem=strict 会让该路径只读,而 connect() 一个 unix socket 需要写权限ReadWritePaths=/var/lib/rha-dbus
验证:rha doctor 的蓝牙项应该从 [WARN] bluetooth: bluetooth check unavailable 变成
[OK] bluetooth: default adapter present。别忘了容器里还要
apt install libdbus-1-3,且二进制必须是 gnu 变体。
四、凭据怎么放
Section titled “四、凭据怎么放”配置里任何字符串都可以写 ${VAR},加载时从环境变量取值:
[api]token = "${RHA_TOKEN}"
[[device]]name = "plug_console"adapter = "miot"token = "${MIOT_TOKEN_PLUG}"变量没定义就是加载失败(undefined environment variables: RHA_TOKEN, ...),不会
静默变成空串。
配套的是 systemd 的 EnvironmentFile,unit 里已经写好了:
EnvironmentFile=-/etc/rha/rha.env文件建成 0640 root:rha——root 写,服务用户读,别人看不见:
RHA_TOKEN=...MIOT_TOKEN_PLUG=...THERMO_BINDKEY=...前缀那个 - 不能删。没有它,文件不存在时 systemd 视为致命错误,unit 直接起不来
(lxc-setup.sh 只创建空占位文件,内容由你填)。
[api].token 有非空 + 最短 16 字符的校验,太短或为空的配置起不来:
rha.toml: [api].token 太短或为空(至少需要 16 个字符) —— 弱/空 token 等于不设防,局域网里任何人都能读写全屋状态; 用 `openssl rand -hex 16` 生成一个这条是 0.11.0 加的(面板的会话 cookie 用它做 HMAC 签名),从旧版本升上来的部署要先跑
一遍 rha check 再重启。
五、改了配置:热加载还是重启
Section titled “五、改了配置:热加载还是重启”热加载现在覆盖设备的增、删、改,不只是规则。 加一台温湿度计不需要重启整个 daemon ——既有设备的连接、会话、轮询节奏都不受影响,只有新那台会起来。
rha reload # 走 APIsystemctl reload rha # 等价,unit 的 ExecReload 是 kill -HUPkill -HUP <pid> # 同上| 改了什么 | 怎么生效 |
|---|---|
automations/*.toml 规则与命名动作 |
reload |
devices.toml 增设备 / 删设备 / 改参数 |
reload |
rha.toml 的 [api] / [storage] |
systemctl restart |
rha.toml 的 [llm] / [notify] |
systemctl restart |
bridges.toml |
systemctl restart |
spec.d/*.toml 型号表 |
systemctl restart |
校验全过才切换,失败时旧配置继续跑,不存在一次 reload 把系统搞挂的路径——和 nginx 拒绝加载坏配置是同一个语义。删设备时如果还有规则引用它的实体,整份配置会被拒绝,错误里 点名是哪条规则挡着。
改参数(比如设备 IP 变了)等价于只重启那一台。判据取自 devices.toml 里这一条的
原文,所以改 label 也算改——代价是显示名一变会让这台设备重连一次,换来的是判据
只有一条、不可能漏掉某种改动而静默失效。
spec.d/ 要重启是刻意的:reload 用的是启动时读进内存的那份型号表,跑着的 miot
adapter 手里就是它。拿新读的表去校验会放行一台运行期根本认不出型号的设备——它会一遍遍
启动失败,而 reload 报的是成功。
# 1. 取 CI artifact,推到容器(先推成另一个名字,服务不受影响)pct push 140 rha /usr/local/bin/rha.new --perms 0755
# 2. 新二进制 + 现有配置先验一遍pct exec 140 -- bash -lc 'set -a; . /etc/rha/rha.env; set +a /usr/local/bin/rha.new --config /etc/rha check'
# 3. 留一份回滚用的,再换上去pct exec 140 -- bash -lc 'cp /usr/local/bin/rha /usr/local/bin/rha.prev mv /usr/local/bin/rha.new /usr/local/bin/rha systemctl restart rha'先 check 再替换:配置和新二进制不兼容时不至于把服务弄挂。第 2 步里服务仍然跑着内存 里的旧代码,换文件也不影响它。
回滚就是 mv /usr/local/bin/rha.prev /usr/local/bin/rha && systemctl restart rha。
storage 是 SQLite 单文件,跨大版本回滚前先 cp 一份 history.db。
systemctl stop|restart 不会硬杀进程
Section titled “systemctl stop|restart 不会硬杀进程”发的是 SIGTERM,rha 走的是优雅关停:先给 systemd 发 STOPPING=1,再取消所有任务、
让 adapter 收尾。实测 702ms 干净退出,全程没有 SIGKILL。所以升级期间正在写的历史点不会
被截断,设备连接也是正常断开而不是被拔线。
(kill -9 当然还是硬杀,那条路径靠 Restart=always 兜底,实测能自动拉起。)
涉及新配置字段时,先换二进制再改配置
Section titled “涉及新配置字段时,先换二进制再改配置”[api] 那段是 deny_unknown_fields——不认识的字段直接报错。反过来做的话,从「改完配置」
到「换完二进制」这段窗口里任何一次重启,旧二进制都会因为不认识新字段而起不来。
唯一的例外是 spec.d/ 型号表,顺序正好相反:新二进制需要
spec.d/ 才能建出 miot 实体,而旧二进制完全无视这个目录。所以先 rha spec-gen 生成
表、再换二进制。
用了 HomeKit 桥的话,升级后必须确认 c# 递增了
Section titled “用了 HomeKit 桥的话,升级后必须确认 c# 递增了”c# 是 HAP 的配置版本号,iOS 只在它变化时才重新拉取配件表。配件表指纹里含
FirmwareRevision,而那个值就是 rha 的版本号——所以每次升级都必然让 c# 加一。
升级后日志里应该有这一行:
配件表有变化, c# 已递增, iid 换用新号段没有这行就是出问题了:iOS 手里还是旧配件表,而配件的编号(iid)已经换了含义,现象
是「家庭」里全部 No Response。这条不是理论——0.7.0 就因为这个全灭过一次,修法是让 iid
号段随 c# 整体前进,保证一个 iid 永远不改变含义。
顺带:升级后 iOS 会多拉一次配件表,这是 c# 递增的正常代价,不用管。
七、验证装对了没
Section titled “七、验证装对了没”三层,从不碰网络到碰网络:
-
rha check——完全离线。 校验整份配置,包括「规则引用的实体存不存在」。改完配置 都先跑这条,全过才谈别的。Terminal window rha --config /etc/rha check# config ok: 39 entities, 7 rules -
rha doctor——会探设备可达性。 六类检查:配置能不能加载、spec.d/有没有型号、 adapter 认不认得、storage 路径写不写得了、蓝牙适配器在不在、每台设备通不通,以及 token 格式。Terminal window rha --config /etc/rha doctor# [OK] config: 39 entities, 7 rules# [OK] miot-spec: 12 个型号# [OK] adapter-registration: 11 device(s), all adapters registered# [WARN] bluetooth: bluetooth check unavailable (needs a linux build with the mibeacon feature)# [OK] device-reachability: plug_console (192.168.52.124:54321) responded任一项 FAIL 时退出码是 1(WARN 不算),可以直接串进部署脚本。
-
看日志。
journalctl -u rha -f。启动正常的标志是rha listening on ...加notified systemd READY=1;systemctl status rha显示Started而不是activating就说明Type=notify那条链路是通的。
八、它到底占多少资源
Section titled “八、它到底占多少资源”这几个数字出自 2026-07-26 的首次部署实测:N150 小主机 / PVE 9.2.3 / CT 140,Debian 13 非特权 LXC,256M / 2 core / 4G,跑的是 musl 静态无 BLE 变体。
| 项 | 实测 |
|---|---|
| 稳态 RSS | 11.8M |
| 峰值 RSS | 12.2M |
| 整个容器(含 systemd、journald) | 17M(配额 256M) |
| 容器重启后拉起 | 1 秒内(onboot + systemctl enable) |
| SIGTERM 到进程退出 | 702ms,无 SIGKILL |
所以容器配额收到 128M 是够的,256M 只是首次部署时给 apt 留的余量。接 BLE 之后换成了 glibc 变体,那一版没有重新量过——多出来的是 D-Bus 客户端和 BLE 中心 Hub,量级上不改变 结论,但要精确数字的话得自己在目标机上再量一次。