自托管服务备份体系设计:每日自动备份与恢复演练
「只备份不演练等于没备份」。本文讲每日自动备份脚本的设计、备份覆盖范围的选择,以及定期恢复演练中容易暴露的常见问题。
备份策略总览
备份目录: /var/backups/stacks/
├── daily/ # 每日备份(保留 7 天)
│ ├── backup_20260907_030000.tar.gz # 配置 + 全库 mysqldump
│ ├── vol_app_mariadb-data_*.tar.gz # MariaDB 数据卷
│ ├── vol_app_redis-data_*.tar.gz # Redis 数据卷
│ ├── vol_app_db_*.tar.gz # 应用 SQLite 卷
│ └── vol_app_media_*.tar.gz # 应用媒体文件卷
└── weekly/ # 每周备份(保留 28 天,周日生成)
├── backup_20260907_030000.tar.gz # 同 daily,多保留一份
└── app-static-src-2026-09-07.tar.gz # 静态资源源文件
典型的做法是在凌晨 3 点由 crontab 触发,整个过程约 5 分钟。
备份脚本五步走
第1步:MariaDB 全库 dump
docker exec mariadb mysqldump -uroot -p"$DB_ROOT_PASSWORD" \
--all-databases --single-transaction --routines --triggers \
> "${STAGING_DIR}/mysql_all_databases_${DATE}.sql"
--single-transaction 保证 InnoDB 表的一致性快照,不锁表。--routines --triggers 连存储过程和触发器一起导出。
安全要点:数据库密码从 .env 文件读取(600 权限,不进 git),脚本内绝不硬编码。读不到就报错退出,不降级继续跑——备份缺了数据库等于没备份。
第2步:打包所有 compose 配置
遍历 /srv/stacks/*/ 下所有服务目录,复制 compose 文件和配置文件(.yml、.conf、.env、.toml、证书等),排除代码目录、node_modules、vendor、.git。
for dir in /srv/stacks/*/; do
service=$(basename "$dir")
[[ "$service" =~ ^(backups|scripts|logs)$ ]] && continue
if [ -f "${dir}docker-compose.yml" ]; then
# 复制 compose + 配置文件
find "$dir" -maxdepth 3 \
\( -name "*.yml" -o -name "*.env" -o -name "*.conf" ... \) \
-not -path "*/node_modules/*" -not -path "*/.git/*" \
| while read -r f; do cp "$f" "${svc_dir}/${rel}"; done
fi
done
第3步:打包每日备份
把数据库 dump + 所有配置打包成一个 backup_*.tar.gz。
第4步:备份 Docker 数据卷
关键数据在 Docker 命名卷里,必须单独备份:
VOLUMES=(
"app_mariadb-data" # MariaDB 数据目录
"app_redis-data" # Redis 持久化数据
"app_db" # 应用 SQLite 数据库
"app_media" # 应用上传的媒体文件
)
备份方式是启动一个临时 Alpine 容器,挂载目标卷(只读)和备份目录,用 tar czf 打包:
docker run --rm \
-v "${vol}":/source:ro \
-v "${BACKUP_ROOT}":/backup \
alpine tar czf "/backup/daily/vol_${vol}_${DATE}.tar.gz" -C /source .
第5步:清理旧备份 + 每周归档
- 日备:
daily/下超过 7 天的文件自动删除。 - 周备:每周日把当日备份复制到
weekly/,额外归档应用静态资源源目录,保留 28 天。
哪些不备份?为什么
一个关键决策是不备份静态资源产物数据卷。
这个卷是 collectstatic 的产物——应用每次容器启动时,entrypoint.sh 会自动跑 collectstatic 从源码重新生成。它是可再生数据,不需要备份。而且带哈希文件名的压缩存储会生成额外的副本,体积明显翻倍,每日备份纯属浪费空间。
但源文件(app/static/ 目录)不在 git 里(被 .gitignore 排除了),也不在数据卷里(卷里只有产物),所以改为每周归档源目录作为兜底。
决策框架:
| 数据类型 | 备份策略 | 理由 |
|---|---|---|
| 数据库(MariaDB/SQLite/Redis) | 每日 | 不可再生,丢失即灾难 |
| 配置文件 | 每日 | 改了就丢了 |
| 媒体文件(用户上传) | 每日 | 不可再生 |
| 静态资源产物 | 不备份 | collectstatic 自动重建 |
| 静态资源源文件 | 每周 | 不在 git 里,需要兜底 |
| 代码目录 | 不备份 | 在 git 仓库里 |
安全加固:umask 077
备份产物包含数据库全量 dump(含 mysql.user 表的密码哈希),必须严格控制权限:
# 脚本开头设置 umask,确保所有生成的文件 600、目录 700
umask 077
默认 umask 022 会生成 644/755 权限的文件,任何用户都能读。收紧到 077 后,只有 root 能读写备份文件。
恢复脚本设计
restore.sh 根据文件名自动判断备份类型:
FILENAME=$(basename "$BACKUP_SOURCE")
if [[ "$FILENAME" == mysql_* ]]; then
# MariaDB SQL dump → docker exec -i mariadb mysql
elif [[ "$FILENAME" == vol_* ]]; then
# Docker 数据卷 → alpine tar xzf 到目标卷
elif [[ "$FILENAME" == backup_*.tar.gz ]]; then
# 完整配置备份 → 交互式选择恢复配置/数据库/全部
fi
完整配置备份的恢复支持三种模式:
- 仅恢复配置文件:把
service-configs/下的配置复制回各服务目录。 - 仅恢复数据库:从 SQL dump 导入。
- 全部恢复:配置 + 数据库一起恢复。
每种模式都有确认提示(--force 可跳过),防止手滑覆盖生产数据。
恢复演练:容易暴露的问题
定期做恢复演练是备份体系的关键环节。一个可复用的演练流程:
- 把备份文件拉到一台干净环境(或本地虚拟机)。
- 按文档一步步还原:创建 Docker 卷 → 恢复数据卷 → 恢复数据库 → 启动服务。
- 逐表记录数比对——这是最硬的验证手段:
# 生产环境
docker exec mariadb mysql -uroot -p -e "SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='appdb';" > prod_counts.txt
# 恢复环境
docker exec mariadb mysql -uroot -p -e "..." > restored_counts.txt
# 比对
diff prod_counts.txt restored_counts.txt
- 验证服务能正常启动、页面能打开、数据一致。
演练中常见的三类问题
问题一:备份目录路径不匹配
健康检查脚本 health-check.sh 里的 BACKUP_DIR 指向的目录,和备份脚本实际产出的目录不一致(例如一个写死 /srv/stacks/backups/,实际产物在 /var/backups/stacks/daily/)。结果健康检查一直报「最近 24h 备份未找到」——不是没备份,是检查的目录不对。统一备份目录常量、让检查脚本和备份脚本引用同一个来源,是最简单的规避方式。
问题二:数据卷清单遗漏
备份脚本里手工维护的数据卷列表,很容易漏掉后来新增的卷——例如某个后来新增的有状态服务的数据卷。演练时才发现对应文件在恢复后丢失,才补进清单。每次新增有状态服务,都要同步更新备份卷清单。
问题三:凭据硬编码风险
一种常见风险是:把数据库 root 密码硬编码进 backup.sh / restore.sh 作为回退值。如果这些脚本处于 git 跟踪范围内,凭据就可能随代码一起被推送出去。正确做法是只从 .env 文件读取,读不到就报错退出,绝不降级;一旦怀疑凭据外泄,应立即轮换。
教训:凭据唯一来源应该是权限受控的 .env 文件(600 root:root,被 .gitignore 排除),脚本里读不到就报错退出,绝不降级。
备份完整性监控
健康检查脚本每天凌晨 2:10 跑一次(备份脚本 3:00 跑),检查 BACKUP_DIR 下 24 小时内是否有 backup_*.tar.gz(配置 + 数据库 dump)和至少一个 vol_*.tar.gz(数据卷)。两者都有才算完整。
cfg_count=$(find "$BACKUP_DIR" -name 'backup_*.tar.gz' -mtime -1 | wc -l)
vol_count=$(find "$BACKUP_DIR" -name 'vol_*.tar.gz' -mtime -1 | wc -l)
if (( cfg_count > 0 && vol_count > 0 )); then
result ok "最近24h备份" "配置 ${cfg_count} 个 + 数据卷 ${vol_count} 个"
else
result warn "最近24h备份" "产物不完整"
fi
小结
备份体系的五条原则:
- 备份要全覆盖:数据库 + 配置 + 数据卷 + 不可再生的用户数据,一个都不能漏。
- 备份要能恢复:定期演练,逐表记录数比对确认零丢失。
- 权限要收紧:
umask 077,备份文件只有 root 可读。 - 凭据不硬编码:从
.env读,读不到就报错,不降级。 - 可再生数据不备份:静态资源产物、代码目录等,避免浪费存储。
下一篇讲 Uptime Kuma 监控体系的搭建。
评论 (0)
暂无评论,来说点什么吧。
登录 后可以发表评论