飞牛 OS 使用要点
来源:
halo-kb/scripts/nas/README.md(整篇)(原文 6934 字符)
Halo 备份 / 恢复 / 运维脚本(NAS 侧)
目标环境(已实测):
| 项 | 值 |
|---|---|
| 主机 | 飞牛 OS(FNOS),Debian 12 用户态,systemd 252,bash 5.2.15 |
| 项目目录 | /vol1/1000/docker/halo |
| 容器 | Halo(registry.fit2cloud.com/halo/halo-pro:2.26)、PostgreSQL(postgres:15.4) |
| 备份根目录 | /vol1/1000/docker/halo/backups(只增不改的约定位置) |
| 执行身份 | ZYJ(uid 1000,在 docker 组内,无需 sudo) |
一、文件清单
| 文件 | 作用 | 是否需要 root |
|---|---|---|
halo-backup.sh | 全量备份:pg_dump -Fc + pg_dumpall --globals-only + halo2/ 归档 + compose/env 副本 + 校验和 + 清单 + 保留策略 | 否 |
halo-restore-verify.sh | 非破坏性恢复验证:把 dump 恢复到临时库、比对计数、删临时库 | 否 |
halo-restore.sh | 破坏性生产恢复(有交互确认与 --confirm-restore 双闸) | 否 |
halo-healthcheck.sh | 只读健康检查(10 项判据),支持 --prom / --quiet | 否 |
halo-backup.cron | 用户级 crontab:每日备份 + 每周恢复验证 + 每日两次健康检查 | 否 |
halo-logrotate.conf | halo2/logs/halo.log 的轮转规则(copytruncate,不重启容器) | 是(装到 /etc/logrotate.d/) |
install-on-nas.sh | 部署脚本 + 语法/依赖自检 + 打印后续步骤 | 否 |
二、部署到 NAS
本机(Windows)把脚本目录送到 NAS。任选一条:
方式 A:本机起临时 HTTP 服务,NAS 主动拉取(实测可用,最快)
# 本机(H:\Works\halo-kb\scripts\nas 目录下)
python -m http.server 8899 --bind 0.0.0.0# NAS 上
curl -s -o /tmp/halo-scripts.tar http://<内网IP>:8899/halo-scripts.tar
mkdir -p /tmp/halo-scripts && tar -xf /tmp/halo-scripts.tar -C /tmp/halo-scripts
bash /tmp/halo-scripts/install-on-nas.sh⚠️ 本机防火墙需放行 8899;用完把 HTTP 服务停掉。 ⚠️ 本机 IP 会变(实测
<内网IP>;WLAN 上是<内网IP>),换网段要一起改。
方式 B:用 NAS 文件管理器 / SMB 手抄 —— 把本目录文件复制到 /vol1/1000/docker/halo/scripts/,然后
chmod 750 /vol1/1000/docker/halo/scripts/*.sh
bash /vol1/1000/docker/halo/scripts/halo-backup.sh --dry-run # 自检安装后目录应为:
/vol1/1000/docker/halo/scripts/
├── halo-backup.sh (750)
├── halo-restore-verify.sh (750)
├── halo-restore.sh (750)
├── halo-healthcheck.sh (750)
├── halo-backup.cron
├── halo-logrotate.conf
└── README.md三、日常使用
cd /vol1/1000/docker/halo
./scripts/halo-backup.sh --dry-run # 只打印计划,不落盘
./scripts/halo-backup.sh # 正式备份(实测约 5.5 秒)
KEEP=14 ./scripts/halo-backup.sh # 覆盖保留份数(默认 7)
INCLUDE_LOGS=1 ./scripts/halo-backup.sh # 把 halo2/logs 也打进归档(默认排除)
./scripts/halo-restore-verify.sh # 用最新 dump 做非破坏性恢复验证(约 44 秒)
./scripts/halo-healthcheck.sh # 10 项健康检查
./scripts/halo-healthcheck.sh --prom # Prometheus textfile 格式
./scripts/halo-healthcheck.sh --quiet # 只在失败时输出(给 cron 用)产物命名(同一时间戳一套):
halo-db-<ts>.dump PostgreSQL custom 归档(-Fc)
halo-pg-globals-<ts>.sql 全局角色(CREATE ROLE / ALTER ROLE)
halo2-<ts>.tar.gz Halo 工作目录(排除 backups/ 与 logs/)
compose-<ts>.yaml docker-compose.yaml 原样副本
container-env-<ts>.txt 两容器环境变量快照(口令一律脱敏)
MANIFEST-<ts>.txt 清单:计数、镜像 digest、恢复要点
MANIFEST-<ts>.sha256 四个产物的 sha256
backup.log 追加式运行日志回滚一次备份产物(确认无用后):
rm -f backups/halo-db-<ts>.dump* backups/halo2-<ts>.tar.gz* \
backups/halo-pg-globals-<ts>.sql* backups/compose-<ts>.yaml* \
backups/container-env-<ts>.txt backups/MANIFEST-<ts>.*四、恢复步骤
4.1 先验证,再恢复(必做)
./scripts/halo-restore-verify.sh判据:输出 ✅ 恢复验证通过,且「表数量」「extensions 行」两侧一致。 这一步不改生产库,随时可跑。
4.2 灾难恢复(会覆盖生产数据)
cd /vol1/1000/docker/halo
# 0) 先给「恢复前的现状」再做一份备份,别把唯一的退路也覆盖掉
./scripts/halo-backup.sh
cp -p backups/MANIFEST-<ts>.txt /tmp/restore-plan.txt # 记下这次的时间戳
# 1) 停 Halo(不动数据库容器)
docker compose stop halo
# 2) 恢复数据库
docker exec -i PostgreSQL pg_restore -U halo -d halo --clean --if-exists --no-owner \
< backups/halo-db-<ts>.dump
# 3) 恢复文件(会覆盖 halo2/,故第 0 步不可省)
tar -xzf backups/halo2-<ts>.tar.gz -C /vol1/1000/docker/halo
# 4) 恢复 compose(仅在 compose 本身也丢了/被改坏时)
cp -p backups/compose-<ts>.yaml /vol1/1000/docker/halo/docker-compose.yaml
# 5) 起 Halo 并验证
docker compose up -d halo
./scripts/halo-healthcheck.sh4.3 ⚠️ 三个会让人恢复错库/丢数据的坑(都是实测结论)
- 本机宿主上另有一个 PostgreSQL。
宿主 postgres 系统用户(uid 107)监听 127.0.0.1:5432,且宿主装了 /usr/bin/psql。 在 NAS 上裸跑 psql 会连到它,而不是 Halo 的库。 本方案所有数据库命令一律显式走 docker exec PostgreSQL ...,不要用宿主 psql。 (另有容器 n8n-postgres,当前 Exited,同样不要碰。)
- 口令不在脚本里,靠的是容器内本地 socket 的
trust认证。
pg_hba.conf 里有 local all all trust,所以 docker exec PostgreSQL pg_dump -U halo ... 不需要任何口令。这也意味着任何能在该容器内执行命令的人都能读全库 —— 见报告 C-3。
--clean --if-exists是破坏性的。
它对目标库逐对象 DROP 再 CREATE。务必先确认 -d 后面的库名是 halo。
五、定时任务
crontab /vol1/1000/docker/halo/scripts/halo-backup.cron
crontab -l内容(详见 halo-backup.cron):
30 3 * * * halo-backup.sh # 每天 03:30 备份
30 4 * * 1 halo-restore-verify.sh # 每周一 04:30 恢复验证
0 8,20 * * * halo-healthcheck.sh --quiet # 每天 08:00/20:00 健康检查验证它真的会跑(三条,缺一不算验证):
crontab -l # ① 条目在
date # ② 宿主机时区(cron 用系统时区,不换算)
grep -n 'cron' /var/log/syslog | tail # ③ cron 是否真的触发了该作业
ls -lt /vol1/1000/docker/halo/backups/ | head # ④ 次日看有没有新时间戳的产物⚠️ 不能用
journalctl验证:ZYJ不在systemd-journal组,实测journalctl返回权限拒绝。 cron 的日志走 syslog。若 syslog 里也看不到,用一个临时作业自证:* * * * * date >> /tmp/cron-alive.log,等两分钟后检查,验完删掉该行。
systemd timer 备选(若你更愿意用 systemd)
需要 root(sudo 在这台 NAS 上要密码,本方案没有代做):
# /etc/systemd/system/halo-backup.service
[Unit]
Description=Halo backup
[Service]
Type=oneshot
User=ZYJ
Group=Users
ExecStart=/vol1/1000/docker/halo/scripts/halo-backup.sh# /etc/systemd/system/halo-backup.timer
[Unit]
Description=Daily Halo backup
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload && sudo systemctl enable --now halo-backup.timer
systemctl list-timers halo-backup.timer # 验证六、日志轮转
sudo cp /vol1/1000/docker/halo/scripts/halo-logrotate.conf /etc/logrotate.d/halo
sudo chown root:root /etc/logrotate.d/halo && sudo chmod 644 /etc/logrotate.d/halo
logrotate -d /etc/logrotate.d/halo # 只演练,判定应显示 after 1 days
sudo logrotate -v -f /etc/logrotate.d/halo # 强制执行一次
ls -la /vol1/1000/docker/halo/halo2/logs/ # 应出现 halo.log-YYYYMMDD判据:-d 输出必须是 after 1 days (14 rotations) 且带 log files >= 20971520 are rotated earlier。 若显示 20971520 bytes (14 rotations) 则配置写错了 —— 见 halo-logrotate.conf 头部两条实测要点。
Docker 侧的 json-file 日志不需要额外处理:
daemon.json已设max-size=100m max-file=5,Halo 容器的HostConfig.LogConfig也显式带了同样参数。 若日后要改这两个值,必须重建容器(docker compose up -d --force-recreate halo), 因为容器的日志驱动参数在创建时就固定了,restart不生效。
七、契约
- 本目录脚本不含任何明文口令或 token:数据库访问走容器内本地 socket(
trust),
容器环境变量快照一律经 sed 脱敏后才落盘。
- 备份产物只写
/vol1/1000/docker/halo/backups/(可用BACKUP_DIR覆盖)。 halo-backup.sh只做新增;唯一的删除行为是保留策略清理它自己产生的旧产物。- 恢复验证用临时库,跑完即删,生产库全程只读。
来源:
intranet-tunnel/docs/AGENTS-ARCHIVE.md→### 三条可复用的判据(原文 1043 字符)
三条可复用的判据
TUNNELS与TUNNELS_FILE能力不同(文档一度把两者写成同一件事):
TUNNELS 只认 name:type:local_port[:domain] 简写 —— 写 JSON 会报 config: TUNNELS 项 ... 的本地端口非法;而且它的 local_addr 被硬编码为 127.0.0.1 (client/internal/config/config.go 的 ParseInlineTunnels 里 LocalAddr: "127.0.0.1")。 容器里的 127.0.0.1 是容器自己 ⇒ 容器场景只能用 TUNNELS_FILE。 已回写 client/.env.docker、deploy/docker/client/client.env.example 与该目录 README (原先两处的 JSON 示例照抄必然启动失败)。
- 域名按
custom_domain全库查占用,跨客户端会被拒绝:ensureStaticTunnels里
GetTunnelByDomain 命中且 owner.ClientID != clientID 时跳过该静态隧道 (日志 域名已被其它客户端占用,忽略静态隧道)。实测 NAS 上 fn(5666)、 qb(8085)、portainer 已被离线客户端 pc1 占着,所以 NAS 客户端只能用新前缀 (本次用 fnos、moviepilot)。
- 落在泛域内的新隧道,云端零改动:域名按前缀声明、服务端用
TUNNEL_DOMAIN补全,
泛域块复用 ⇒ 边缘配置指纹不变、reload 次数不增,内置反代规则也随登录自动装载 (本次没有触发 POST /api/proxies/reload,也没有重启 tunnel-server)。 改完 tunnels.json 只需 docker compose restart tunnel-client; 而改 client.env 仍必须 up -d --force-recreate(env_file 只在创建容器时读)。
