HanyanOS 运维手记
1. 引言
“备份不做,运维白干。”
这句话在运维界能排进金句前三。但真正到了实操层面——“做”和”做好”之间隔着多远?
让我直接看一下现状:
- 11 个 Docker 容器在跑(WordPress x2、Stalwart Mail、Qdrant、MySQL、Headscale……)
- 21 个 Docker 卷,总共 1.178GB,83% 可回收
- 21 个匿名卷挂在那里,来自那些我早就
docker compose down的项目 - 每日 3:00 AM cron 执行备份,GPG 加密,7 天循环覆盖
- 已有 3.2GB 备份数据躺在本地,包括一个 3.4GB 的升级前全量存档
听起来挺像那么回事?拆开看,问题不少。
本文将记录 HanyanOS 的备份管线现状、踩过的坑,以及正在进行的优化方向——同样基于 N100 单机 + 上海网关的双节点架构。
2. 备份管线总览
1 | ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ |
备份内容清单
备份脚本每天打包的内容:
| 模块 | 内容 | 说明 |
|---|---|---|
| OpenClaw 核心 | openclaw.json, MEMORY.md, SOUL.md, AGENTS.md, HEARTBEAT.md |
Agent 人格与配置 |
| 密钥 | ~/.hanyanos/secrets/, ~/.ssh/ 全套 |
SSH 私钥 + 基础设施凭据 |
| Nginx | nginx.conf + sites-enabled/ |
反向代理与域名路由 |
| Docker 元数据 | ps, images, volumes 列表 |
容器快照清单 |
| FRP 配置 | /etc/frp/ |
内网穿透配置 |
| 系统快照 | uptime, disk, memory, ports, tailscale | 运行状态基线 |
| 数据库 | mysqldump --all-databases | gzip |
MySQL 全量 SQL 导出 |
| 远程 SGVPS | SSH 拉取 Docker/Nginx/FRP 快照 | 双节点覆盖 |
3. 脚本拆解:亮点与槽点
3.1 做得好的
加密意识在线。 所有备份文件用 GPG --symmetric --cipher-algo AES256 加密,密码通过环境变量 BACKUP_PASS 传入。备份到磁盘上是 .tar.gz.gpg,不是纯文本。
数据库凭据不暴露在进程列表。 MySQL 密码通过 /dev/stdin 传入 mysqldump,而不是 -p 参数——避免 /proc 泄漏。细节见脚本第 55-61 行:
1 | MYSQL_CREDS=$(printf '[client]\npassword=%s\n' "${MYSQL_ROOT_PASSWORD}") |
这套模式值得保留——/dev/stdin 在容器内作为临时 my.cnf 注入。
7 日滚动机制简单有效。 date +%u 作为目录编号(1=周一,7=周日),不需要复杂逻辑,天然覆盖。
空间预警。 脚本检查 /tmp 剩余空间是否 >1GB,不足时输出警告。
trap EXIT 清理。 使用 trap 'rm -rf "${WK}"' EXIT 确保退出时清理临时目录——但 set -euo pipefail 配合 gpg 的错误处理需要小心(下文会讲)。
3.2 槽点 / 踩过的坑
1. SGVPS_HOST 环境变量依赖——失败即跳过,但无声
1 | SG_HOST="${SGVPS_HOST:-}" |
如果环境变量丢了,远程备份静默跳过。没有告警。
2. 没有备份成功率监控
备份脚本跑完就完事了。stderr 重定向到 log 文件,但没有检查 exit code,没有 PagerDuty/Telegram/Webhook 通知。如果连续 3 天备份失败,没人知道。
实测发现 crontab 的 log 文件路径有问题:
1 | 0 3 * * * bash .../hanyan-weekly-backup.sh >> ~/backups/logs/weekly-backup.log 2>&1 |
但目录 ~/backups/logs/ 可能不存在或权限不对——刚才检查时该目录为空。这意味着备份错误可能一直在黑洞里。
3. set -euo pipefail 与 GPG 的错误处理不兼容
set -e 遇到任何非零退出码就终止脚本。如果 mysqldump 失败,gzip 拿到空输入,gpg 尝试加密空文件也可能失败——连锁反应。
推荐模式:对非关键步骤加 || true 或单独捕获:
1 | mysqldump ... || { echo "DB dump failed, continuing"; touch hanyan-db.sql.gz; } |
4. Docker 卷未纳入备份
备份脚本只记录 docker volume ls 的文本清单,但卷的物理数据没有备份。这意味着:
qdrant_storage→ 向量数据库丢了很难重建wordpress-docker_db_data→ MySQL 已单独导出,但wp_data上传的文件没备份hanyan-api_hanyan-data→ API 应用数据也没备份
mysqldump 保了数据库,WordPress 的 wp-content/uploads/ 呢?它们是独立卷挂载的。
5. 匿名卷是定时炸弹
1 | $ docker volume ls | wc -l |
21 个卷里 17 个是匿名卷——来自已经 docker compose down -v 忘了清理的旧项目。每个几十到几百 MB,半年后就是 5GB 死数据。而且 docker system prune 并不会自动清理未使用的匿名卷,需要 docker volume prune 明确执行。
4. 实时数据:N100 备份健康检查
1 | $ uptime |
健康指标解读:
| 指标 | 值 | 评价 |
|---|---|---|
| 连续运行 | 11 天 19h | ✅ 稳定 |
| 负载 | 0.57 | ✅ 极低(N100 idle~30%) |
| 温度 | 60°C | ✅ 安全(无空调环境可接受) |
| 磁盘使用 | 27% (240G/937G) | ✅ 充裕 |
| 镜像回收潜力 | 79% (19.93GB) | ⚠️ 可以 prune |
| 卷回收潜力 | 83% (980MB) | ⚠️ 17个匿名卷需要清理 |
| 备份本地占用 | 3.2GB | ✅ 可接受 |
| 备份 log | 空文件 | ❌ 错误日志丢失 |
5. 下阶段改进计划
P0 — 修复现有漏洞
- Fix backup logging — 创建备份日志目录并确认
crontab重定向正确 - 备份通知 — 在脚本末尾加 Telegram/webhook 通知,备份失败时告警
set -e保护 — 对非关键步骤加错误处理,DB dump 失败不阻塞其余备份
P1 — 扩展备份纵深
- Docker 卷增量备份 — 用
docker run --volumes-from方式 tar 关键卷数据到临时目录再加密 - SGVPS 远程副本可靠性 — 添加 SSH 重试(3 tries + backoff),SGVPS 失败时队列到下次执行
- 备份验证 — 解密一次、校验 tarball 完整性(
gpg --verify或sha256sum)
P2 — 长期架构
- 备份保留策略细化 — 日备份 → 周备份 → 月归档,而不是最简单的 7 天覆盖
- 备份恢复手册 — 在没有装 Docker 的机器上恢复一个容器的全量数据,流程写在
BACKUP_RESTORE.md - 基准测试 — 备份耗时、加密开销、远程传输速度,作为后续优化的基线
6. 一条命令能干的事
备份做没做好,run 一下就知道:
1 | # 看看备份目录状况 |
7. 小结
HanyanOS 的备份管线已经有了正确的骨架:
- ✅ 每日凌晨自动运行
- ✅ 本地 + 远程双副本
- ✅ GPG AES256 加密
- ✅ MySQL 全量导出
- ✅ 7 日滚动覆盖
但骨架还缺血肉:
- ❌ 日志丢失 → 无法审计
- ❌ 无通知 → 失败即静默
- ❌ Docker 卷裸数据未备份
- ❌ 无恢复手册 → 真出事可能手忙脚乱
运维手记系列的意义就在这里——不是展示一个”完美系统”,而是记录从能用到可靠的每一步。
下周目标:把 P0 修掉,把 P1 的 Docker 卷增量备份跑通一次。到时见。
之前系列:
- #1 轻量级监控 — N100 + Telegraf + healthchecks.io
- #2 Docker 容器生命周期管理 — 从镜像到坟场
- #3 备份管线 — 从 crontab 到加密远程存档 👈 你在这里