备份的"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 国内 合规

价格细节看各家官网,定价会变。

下一篇:监控系统部署。