备份的"3-2-1"
最低标准:
- 3 份副本
- 2 种介质
- 1 份异地
对个人小项目实操:
- 1 份服务器本地
- 1 份对象存储(异地)
- 偶尔下载一份到本地冷备
备什么 / 不备什么
备
- 数据库导出
- 用户上传的文件
- 应用配置 /
.env(加密后) - 关键证书
不备
- 代码(git 已是备份)
- 容器镜像(可重建)
- 缓存文件
- 日志(大多数情况可弃)
用 rclone 同步到对象存储
sudo apt install rclone
rclone config # 交互式:选 R2 / S3 / OSS / B2 等
配完后:
rclone copy /backups r2:my-backup --progress
rclone sync /backups r2:my-backup --backup-dir r2:my-backup/old-$(date +%F)
完整备份脚本
/usr/local/bin/backup.sh:
#!/bin/bash
#
# 综合备份脚本:数据库 + 文件 + 配置 → 异地对象存储
#
set -euo pipefail
IFS=$'\n\t'
# ─── 配置 ─────────────────────────────────
readonly BACKUP_DIR="/backups"
readonly REMOTE="r2:my-backup"
readonly DATE=$(date +%F)
readonly LOG_FILE="/var/log/backup.log"
readonly RETENTION_DAYS=14
readonly DBS=("myapp" "blog")
readonly DIRS=("/srv/uploads" "/srv/configs")
# ─── 工具函数 ─────────────────────────────
log() { printf "[%s] %s\n" "$(date +'%F %T')" "$*"; }
die() { log "ERROR: $*"; exit 1; }
# 日志输出双写
exec > >(tee -a "$LOG_FILE") 2>&1
cleanup() {
log "清理本地旧备份(> $RETENTION_DAYS 天)"
find "$BACKUP_DIR" -type f -mtime +$RETENTION_DAYS -delete || true
}
trap cleanup EXIT
# ─── 主流程 ───────────────────────────────
main() {
log "===== 备份开始 ====="
mkdir -p "$BACKUP_DIR/$DATE"
# 1. 数据库
for db in "${DBS[@]}"; do
log "备份数据库 $db"
mariadb-dump --single-transaction "$db" \
| gzip > "$BACKUP_DIR/$DATE/db-$db.sql.gz"
done
# 2. 文件目录
for dir in "${DIRS[@]}"; do
local name
name=$(basename "$dir")
log "备份目录 $dir"
tar -czf "$BACKUP_DIR/$DATE/$name.tar.gz" "$dir" 2>/dev/null
done
# 3. 配置(加密)
log "备份 + 加密 /etc/myapp"
tar -czf - /etc/myapp 2>/dev/null \
| gpg --batch --yes --passphrase-file /root/.backup-pass \
--symmetric --cipher-algo AES256 \
-o "$BACKUP_DIR/$DATE/etc-myapp.tar.gz.gpg"
# 4. 推到对象存储
log "推到 $REMOTE"
rclone copy "$BACKUP_DIR/$DATE" "$REMOTE/$DATE" --progress
log "===== 备份完成 ====="
}
main "$@"
可执行 + cron:
sudo chmod +x /usr/local/bin/backup.sh
sudo crontab -e
# 每天凌晨 3 点
0 3 * * * /usr/local/bin/backup.sh
增量备份:rsync
tar 是全量——每天重打一次包。文件多时浪费空间,用 rsync 做增量:
# 第一次:完整复制
rsync -a /srv/uploads/ /backups/uploads/
# 之后:只同步差异
rsync -a --delete /srv/uploads/ /backups/uploads/
--delete 删目标里源没有的——保持镜像。
软链接快照(穷人版"快照")
DATE=$(date +%F)
PREV=$(ls /backups/snapshots/ | sort | tail -1)
# 用 --link-dest 引用上次快照——未变文件硬链接,节省空间
rsync -a --delete \
--link-dest=/backups/snapshots/$PREV \
/srv/uploads/ /backups/snapshots/$DATE/
效果:每天看起来都有完整目录,但未变的文件多个日期共享同一份磁盘数据——空间几乎不增长。
⚠ 测试恢复(最容易被忽视)
"没演练过的备份等于没备份。"
每月做一次:
# 1. 从远端拉一份
rclone copy r2:my-backup/2026-05-01 /tmp/restore-test/
# 2. 在测试机器 / 临时容器恢复
docker run --rm -v /tmp/restore-test:/in mariadb:11 \
bash -c "gunzip < /in/db-myapp.sql.gz | mariadb ..."
# 3. 对比数据完整性
mariadb -e "SELECT COUNT(*) FROM users"
# 对比生产 vs 恢复,数字一致 = OK
发现:
- 备份太慢?需要优化 / 用物理备份
- 恢复时间不达标?算账加机器或换方案
- 备份缺东西?补脚本
加密:保护异地数据
放到云上的备份默认应该假设可能泄露——加密后再上传。
# 对称加密(脚本里用)
gpg --batch --yes --passphrase-file /root/.backup-pass \
--symmetric --cipher-algo AES256 -o file.gpg file
# 解密
gpg --batch --yes --passphrase-file /root/.backup-pass \
--decrypt file.gpg > file
⚠ 加密密钥不能丢——丢了备份等于没了。密钥存到密码管理器(1Password / Bitwarden)+ 至少 2 处冷备。
成本对比(粗略)
| 存储 | 价格区间 | 备注 |
|---|---|---|
| Cloudflare R2 | 便宜 + 出流量免费 | 小项目首选 |
| Backblaze B2 | 最便宜 | 简单 |
| AWS S3 / Glacier | 标准 / 归档分层 | 大规模 |
| 阿里 OSS / 腾讯 COS | 国内 | 合规 |
价格细节看各家官网,定价会变。
下一篇:监控系统部署。