服务器自动备份到 Cloudflare R2 —— 完整教程
适用场景:Debian/CentOS 服务器,网站 + MySQL 数据库,每天自动备份到 Cloudflare R2,R2 超过设定天数自动清理。本教程用通用占位符 your-server(服务器名)、example.com / example_com 代表网站和数据库。举例,实际使用时把这些占位符替换成你自己的值即可,其他服务器照抄一遍,只改第五步和第六步里的几个变量即可。但是注意不要多台服务器共用一个R2 Token!
一、准备工作(只需要做一次,跟服务器无关)
1.1 创建 / 确认 R2 Bucket
Cloudflare Dashboard → R2 → 确认已有一个 bucket,比如 server-backups,专门用来存放所有服务器的备份。多台服务器共用同一个 bucket 也没问题,脚本会按服务器名分文件夹。
1.2 创建 R2 API Token
Cloudflare Dashboard → R2 → 右上角「Manage R2 API Tokens」→ 「Create API Token」。
- 权限选择:Object Read & Write(不要选 Admin Read & Write,服务器只需要读写对象,不需要管理 bucket)
- 作用范围(Bucket scope):限定到
server-backups(或选 Apply to all buckets,图省事也行) - 创建后会看到一次性显示的 Access Key ID 和 Secret Access Key,以及 Endpoint(形如
https://<账户ID>.r2.cloudflarestorage.com),三个都要记下来,Secret 只显示一次,关掉页面就看不到了。
二、安装 rclone
SSH 登录服务器,执行:
curl -fsSL https://rclone.org/install.sh -o /tmp/rclone-install.sh
bash /tmp/rclone-install.sh
rm -f /tmp/rclone-install.sh
rclone --version
最后一条能打印出版本号就说明安装成功。
三、配置 rclone 对接 R2(免交互式菜单,一条命令搞定)
交互式的 rclone config 菜单编号会随版本变化,容易选错,这里用非交互命令直接创建,稳定不会因为版本更新而不一样。
注意:不要把 Secret 明文直接写在命令里执行——那样整条命令(包含明文密钥)会被记录进 ~/.bash_history,以后谁能看到你的 shell 历史谁就能看到密钥。改用 read 交互式输入,密钥不会进历史记录:
read -r -p "R2 Access Key ID: " R2_ACCESS_KEY
read -r -s -p "R2 Secret Access Key: " R2_SECRET
echo
rclone config create R2 s3 \
provider=Cloudflare \
access_key_id="$R2_ACCESS_KEY" \
secret_access_key="$R2_SECRET" \
endpoint="https://你的账户ID.r2.cloudflarestorage.com" \
region=auto \
no_check_bucket=true
unset R2_ACCESS_KEY R2_SECRET
endpoint 里的 你的账户ID 换成 1.2 步拿到的真实值,R2_ACCESS_KEY 和 R2_SECRET 在提示时手动粘贴输入(输入 Secret 时屏幕不会显示字符,属于正常现象,粘贴完直接回车)。最后 unset 是把这两个变量从当前 shell 会话里清掉,避免残留。
执行完检查一下配置是否正确:
rclone config redacted R2
应该看到:
[R2]
type = s3
provider = Cloudflare
access_key_id = XXX
secret_access_key = XXX
endpoint = https://你的账户ID.r2.cloudflarestorage.com
region = auto
no_check_bucket = true
四、测试连通性
不要测试 rclone lsd R2:(这个是列出账户下所有 bucket,需要账户级权限,而我们的 Token 是限定单个 bucket 的,测这个会 403,属于正常现象,不代表配置错了)。
正确的测试方式,直接指定 bucket:
rclone lsd R2:server-backups
没有报错(哪怕没有任何输出,因为桶是空的)就说明配置成功。也可以顺手传一个测试文件验证读写:
echo "test" > /tmp/r2-test.txt
rclone copy /tmp/r2-test.txt R2:server-backups/test/
rclone ls R2:server-backups/test/
rm -f /tmp/r2-test.txt
能看到 r2-test.txt 就说明服务器到 R2 的链路完全打通了。
五、创建备份脚本
mkdir -p /root/scripts /root/backup-tmp
nano /root/scripts/backup.sh
把下面内容整段粘贴进去,只需要改开头「基础配置」这几行:
#!/bin/bash
set -Eeuo pipefail
# =========================
# 基础配置(每台服务器只改这一段)
# =========================
# 服务器名称,用于 R2 中区分不同服务器
SERVER_NAME="your-server"
# R2 remote(必须和 `rclone listremotes` 里显示的名字完全一致,区分大小写)
R2_REMOTE="R2:server-backups"
# 临时备份目录
BACKUP_DIR="/root/backup-tmp"
# 网站列表,可以写多个
WEBSITES=(
"/www/wwwroot/example.com"
)
# 数据库配置,可以写多行
# 格式:"数据库名|数据库用户|网站.env路径"
DATABASES=(
"example_com|example_com|/www/wwwroot/example.com/.env"
)
DATE="$(date '+%Y%m%d-%H%M%S')"
LOG_FILE="/var/log/server-backup.log"
# =========================
# 日志函数
# =========================
log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"
}
# =========================
# 错误处理
# =========================
trap 'log "ERROR: 备份失败,脚本在第 $LINENO 行退出"' ERR
# =========================
# 检查依赖
# =========================
command -v rclone >/dev/null 2>&1 || { log "ERROR: rclone 未安装"; exit 1; }
command -v tar >/dev/null 2>&1 || { log "ERROR: tar 不存在"; exit 1; }
command -v mysqldump >/dev/null 2>&1 || { log "ERROR: mysqldump 不存在"; exit 1; }
# 提前校验 rclone remote 是否配置正确、能否连通 R2(避免跑到一半才 403)
REMOTE_NAME="${R2_REMOTE%%:*}"
if ! rclone listremotes | grep -q "^${REMOTE_NAME}:$"; then
log "ERROR: 未找到名为 ${REMOTE_NAME} 的 rclone remote,请检查 rclone.conf"
exit 1
fi
if ! rclone lsd "${R2_REMOTE}" >/dev/null 2>&1; then
log "ERROR: 无法访问 ${R2_REMOTE},请检查 R2 API Token 权限 / endpoint / provider 配置"
exit 1
fi
mkdir -p "$BACKUP_DIR"
log "======================================"
log "开始服务器备份"
log "服务器:$SERVER_NAME"
log "时间:$DATE"
log "======================================"
# =========================
# 备份网站
# =========================
for SITE_PATH in "${WEBSITES[@]}"; do
if [ ! -d "$SITE_PATH" ]; then
log "WARNING: 网站目录不存在:$SITE_PATH"
continue
fi
SITE_NAME="$(basename "$SITE_PATH")"
LOCAL_FILE="$BACKUP_DIR/${SITE_NAME}-${DATE}.tar.gz"
log "开始备份网站:$SITE_NAME"
tar \
--exclude="${SITE_PATH}/.git" \
-czf "$LOCAL_FILE" \
-C "$(dirname "$SITE_PATH")" \
"$SITE_NAME"
log "网站压缩完成:$LOCAL_FILE"
R2_PATH="$R2_REMOTE/$SERVER_NAME/websites/$SITE_NAME/"
rclone copy "$LOCAL_FILE" "$R2_PATH" --s3-no-check-bucket
log "网站上传完成:$R2_PATH"
if rclone ls "$R2_PATH" | grep -Fq "$(basename "$LOCAL_FILE")"; then
log "网站 R2 验证成功:$SITE_NAME"
else
log "ERROR: 网站 R2 验证失败:$SITE_NAME"
exit 1
fi
rm -f "$LOCAL_FILE"
log "本地网站临时备份已删除:$SITE_NAME"
done
# =========================
# 备份数据库
# =========================
for DB_CONFIG in "${DATABASES[@]}"; do
IFS='|' read -r DB_NAME DB_USER ENV_FILE <<< "$DB_CONFIG"
if [ ! -f "$ENV_FILE" ]; then
log "ERROR: 找不到数据库配置文件:$ENV_FILE"
exit 1
fi
DB_PASSWORD="$(
awk -F= '
/^DB_PASSWORD=/ {
value=$0
sub(/^[^=]*=/, "", value)
gsub(/^["'\'']|["'\'']$/, "", value)
print value
exit
}
' "$ENV_FILE"
)"
if [ -z "$DB_PASSWORD" ]; then
log "ERROR: 无法从 $ENV_FILE 读取 DB_PASSWORD"
exit 1
fi
LOCAL_DB="$BACKUP_DIR/${DB_NAME}-${DATE}.sql.gz"
log "开始备份数据库:$DB_NAME"
MYSQL_CNF="$BACKUP_DIR/mysql-${DB_NAME}.cnf"
cat > "$MYSQL_CNF" <<MYSQL_EOF
[client]
host=127.0.0.1
port=3306
user=${DB_USER}
password=${DB_PASSWORD}
MYSQL_EOF
chmod 600 "$MYSQL_CNF"
mysqldump \
--defaults-extra-file="$MYSQL_CNF" \
--single-transaction \
--quick \
--routines \
--triggers \
--no-tablespaces \
"$DB_NAME" | gzip > "$LOCAL_DB"
rm -f "$MYSQL_CNF"
log "数据库压缩完成:$LOCAL_DB"
R2_DB_PATH="$R2_REMOTE/$SERVER_NAME/databases/$DB_NAME/"
rclone copy "$LOCAL_DB" "$R2_DB_PATH" --s3-no-check-bucket
log "数据库上传完成:$R2_DB_PATH"
if rclone ls "$R2_DB_PATH" | grep -Fq "$(basename "$LOCAL_DB")"; then
log "数据库 R2 验证成功:$DB_NAME"
else
log "ERROR: 数据库 R2 验证失败:$DB_NAME"
exit 1
fi
rm -f "$LOCAL_DB"
log "本地数据库临时备份已删除:$DB_NAME"
done
# =========================
# 清理超过 3 天的本地日志
# =========================
find /var/log -maxdepth 1 -name "server-backup.log.*" -type f -mtime +3 -delete 2>/dev/null || true
log "======================================"
log "全部备份完成"
log "======================================"
保存退出:Ctrl + O → 回车 → Ctrl + X。
赋权限:
chmod 700 /root/scripts/backup.sh
六、手动跑一次,确认没问题再上定时任务
/root/scripts/backup.sh
正常应该看到类似:
开始备份网站:example.com
网站压缩完成
网站上传完成
网站 R2 验证成功:example.com
开始备份数据库:example_com
数据库压缩完成
数据库上传完成
数据库 R2 验证成功:example_com
全部备份完成
如果中途报错,把错误信息发出来,不要直接上 crontab,先确保手动跑能成功。
七、设置每天自动执行(crontab)
crontab -e
第一次用可能会让你选编辑器,选 nano(一般是选项 1)。在文件最后加一行:
0 3 * * * /root/scripts/backup.sh >> /var/log/server-backup-cron.log 2>&1
意思是每天凌晨 3 点自动跑一次,输出额外记一份日志方便排查 cron 有没有正常触发(脚本自己也会写 /var/log/server-backup.log,两份日志内容基本重复,图省事也可以把 >> ... 2>&1 去掉)。
保存退出后确认:
crontab -l
能看到刚加的那一行就说明设置成功了。
八、R2 设置自动过期删除(避免云端无限堆积)
Cloudflare Dashboard → R2 → server-backups → 设置 → 对象生命周期规则 → 新建规则:
- 规则名称:随便写,比如「15天删除一次」
- 规则范围(前缀):留空,代表整个 bucket 都生效(如果以后一个 bucket 放多台服务器,也可以按需要给不同服务器分别设规则,前缀填
服务器名/) - 生命周期操作:只勾选「在此时间之后删除上传的对象」,填
15天(或你想要的天数,建议不要设太短,避免服务器万一连续故障超过这个天数导致所有备份被清空却没人发现) - 其他两个操作(中止未完成的多部分上传 / 转换存储类)不用勾
保存即可,之后 R2 会自动清理超过保留天数的旧备份,不需要脚本自己删。
九、安全提醒
server-backups 这个 bucket 里会包含网站源码、.env、数据库明文数据,务必保持私有(R2 bucket 默认就是私有的,除非你自己手动开了公开访问,正常不用管)。
/root/.config/rclone/rclone.conf 里保存着 R2 的 Access Key / Secret,不要把这个文件传到 GitHub 或发给别人;如果这台服务器的备份脚本要放到代码仓库管理,只放 backup.sh 本身,rclone.conf 和任何密码相关的文件都不要提交。
十、以后再加一台新服务器,怎么做
把这份教程从头到尾重新走一遍,实际上只有三步是每台服务器都要重复的:
- 第二、三、四步:装 rclone、跑那条
rclone config create命令(换成这台服务器自己的 R2 Token,如果多台服务器共用同一个 Token 也可以直接复用)、测试连通。 - 第五步:把脚本抄一份,改
SERVER_NAME、WEBSITES、DATABASES这三处。 - 第六、七步:手动跑一次确认没问题,再加 crontab。
R2 那边(第一步的 Bucket、第八步的生命周期规则)如果所有服务器共用同一个 bucket,只需要设置一次,不用每台服务器重复配置。
0