为什么VPS备份如此重要?

很多VPS用户都有过这样的惨痛经历:一个误操作执行了
1 | rm -rf / |
,或者数据库被勒索病毒加密,又或者云服务商突然发邮件说底层物理机故障需要重建实例——如果没有备份,几个月甚至几年的数据瞬间化为乌有。
根据2025年的一项针对中小型技术团队的调查,超过60%的VPS用户没有完善的自动备份策略,其中近三成的人曾经因数据丢失遭受过重大损失。本文将从零开始,手把手教你搭建一套完整、自动、可靠的VPS数据备份与灾难恢复体系。
无论你是个人开发者还是小团队运维,这套方案都能帮你做到:
- 每天自动备份关键数据,无需人工干预
- 支持本地快照、远程存储、异地容灾三级备份
- 出现故障时能在15分钟内完整恢复
- 备份数据加密传输,确保安全合规
备份策略设计:3-2-1原则
在动手操作之前,我们先来理解业界公认的备份黄金法则——3-2-1备份原则:
| 要素 | 说明 | 我们的实现方案 |
|---|---|---|
| 3份副本 | 数据至少保留3个副本 | 生产数据 + 本地备份 + 远程备份 |
| 2种介质 | 使用至少两种不同的存储介质 | VPS本地磁盘 + 对象存储(S3/OSS) |
| 1份异地 | 至少一份备份在异地 | 跨区域对象存储或另一台VPS |
针对不同类型的VPS数据,我们还需要采用不同的备份频率和保留策略:
| 数据类型 | 备份频率 | 保留周期 | 备份方式 | ||||
|---|---|---|---|---|---|---|---|
| 数据库(MySQL/PostgreSQL) | 每天一次全量 + 每6小时增量 | 30天 |
/
|
||||
| 网站文件(/var/www) | 每天一次 | 14天 |
+ tar压缩 |
||||
| 配置目录(/etc) | 每次变更后(或每天) | 90天 | etckeeper / tar | ||||
| Docker数据卷 | 每天一次 | 7天 | docker volume backup | ||||
| 系统快照 | 每周一次 | 4周 | 云商快照API / LVM快照 |
第一步:本地备份脚本
我们从一个通用的备份脚本开始,它涵盖了VPS上最常见的几类数据。创建
1 | /usr/local/bin/backup.sh |
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128 #!/bin/bash
# VPS通用备份脚本 v2.0
set -euo pipefail
BACKUP_ROOT="/var/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=${RETENTION_DAYS:-14}
BACKUP_NAME="backup_${DATE}"
# 颜色输出函数
info() { echo -e "\033[36m[INFO]\033[0m $1"; }
ok() { echo -e "\033[32m[OK]\033[0m $1"; }
err() { echo -e "\033[31m[ERROR]\033[0m $1" >&2; }
mkdir -p "${BACKUP_ROOT}/${BACKUP_NAME}"
BACKUP_DIR="${BACKUP_ROOT}/${BACKUP_NAME}"
# 1. 备份 MySQL 数据库
backup_mysql() {
if ! command -v mysql &>/dev/null; then
info "MySQL未安装,跳过数据库备份"
return 0
fi
info "开始备份 MySQL 数据库..."
MYSQL_CNF="${HOME}/.my.cnf"
if [ ! -f "$MYSQL_CNF" ]; then
err "未找到 MySQL 配置文件 ${MYSQL_CNF},跳过"
return 1
fi
DATABASES=$(mysql --defaults-file="$MYSQL_CNF" -e \
"SELECT schema_name FROM information_schema.schemata \
WHERE schema_name NOT IN ('mysql','performance_schema','sys','information_schema');" \
-B -N 2>/dev/null) || true
for db in $DATABASES; do
info " 备份数据库: ${db}"
mysqldump --defaults-file="$MYSQL_CNF" \
--single-transaction --quick --lock-tables=false \
--routines --triggers --events \
"${db}" | gzip > "${BACKUP_DIR}/mysql_${db}.sql.gz"
ok " ${db} 备份完成 ($(du -h "${BACKUP_DIR}/mysql_${db}.sql.gz" | cut -f1))"
done
}
# 2. 备份 PostgreSQL 数据库
backup_postgres() {
if ! command -v psql &>/dev/null; then
info "PostgreSQL未安装,跳过"
return 0
fi
info "开始备份 PostgreSQL 数据库..."
PG_USER="${PG_USER:-postgres}"
DATABASES=$(su - "${PG_USER}" -c "psql -t -c 'SELECT datname FROM pg_database WHERE datistemplate = false;'" 2>/dev/null | grep -v '^$' | tr -d ' ') || true
for db in $DATABASES; do
info " 备份数据库: ${db}"
su - "${PG_USER}" -c "pg_dump ${db} --no-owner --compress=9" \
> "${BACKUP_DIR}/pgsql_${db}.sql.gz" 2>/dev/null
ok " ${db} 备份完成 ($(du -h "${BACKUP_DIR}/pgsql_${db}.sql.gz" | cut -f1))"
done
}
# 3. 备份网站文件和配置
backup_files() {
info "开始备份网站文件和系统配置..."
WEBSITES=("/var/www" "/etc/nginx" "/etc/apache2")
for dir in "${WEBSITES[@]}"; do
if [ -d "$dir" ]; then
local name=$(echo "$dir" | tr '/' '_')
tar czf "${BACKUP_DIR}/files${name}.tar.gz" \
--exclude="cache" --exclude="*.log" --exclude="node_modules" \
-C / "${dir#/}" 2>/dev/null
ok " ${dir} 备份完成 ($(du -h "${BACKUP_DIR}/files${name}.tar.gz" | cut -f1))"
fi
done
}
# 4. 备份 Docker 数据卷
backup_docker_volumes() {
if ! command -v docker &>/dev/null; then
info "Docker未安装,跳过"
return 0
fi
info "开始备份 Docker 数据卷..."
VOLUMES=$(docker volume ls -q)
for vol in $VOLUMES; do
info " 备份卷: ${vol}"
docker run --rm -v "${vol}:/source:ro" \
-v "${BACKUP_DIR}:/backup:rw" alpine \
tar czf "/backup/docker_vol_${vol}.tar.gz" \
-C /source . 2>/dev/null
ok " ${vol} 备份完成 ($(du -h "${BACKUP_DIR}/docker_vol_${vol}.tar.gz" | cut -f1))"
done
}
# 5. 生成备份清单
generate_manifest() {
echo "# 备份清单 - ${DATE}" > "${BACKUP_DIR}/MANIFEST.txt"
echo "# 主机: $(hostname)" >> "${BACKUP_DIR}/MANIFEST.txt"
echo "# IP: $(curl -s ifconfig.me 2>/dev/null || hostname -I | awk '{print $1}')" >> "${BACKUP_DIR}/MANIFEST.txt"
echo "---" >> "${BACKUP_DIR}/MANIFEST.txt"
ls -lh "${BACKUP_DIR}" | grep -v MANIFEST >> "${BACKUP_DIR}/MANIFEST.txt"
ok "备份清单已生成"
}
# 6. 清理过期备份
cleanup_old() {
info "清理 ${RETENTION_DAYS} 天前的旧备份..."
find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "backup_*" -mtime "+${RETENTION_DAYS}" -exec rm -rf {} \;
local removed=$(find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "backup_*" -mtime "+${RETENTION_DAYS}" 2>/dev/null)
if [ -z "$removed" ]; then
ok "旧备份清理完成"
fi
}
# 主流程
info "===== 开始 VPS 备份 (${DATE}) ====="
backup_mysql
backup_postgres
backup_files
backup_docker_volumes
generate_manifest
cleanup_old
ok "===== 备份完成 ====="
echo "备份目录: ${BACKUP_DIR}"
du -sh "${BACKUP_DIR}"
保存后赋予执行权限并测试:
1
2
3 chmod +x /usr/local/bin/backup.sh
# 首次运行测试
sudo /usr/local/bin/backup.sh
第二步:远程备份到对象存储
本地备份只解决了”2份副本”中的一部分,我们还需要异地存储来实现完整的3-2-1策略。这里推荐使用 rclone——一个支持几乎所有云存储协议的命令行工具。
安装与配置rclone
1
2
3
4
5
6
7
8
9
10
11
12 # 安装 rclone
sudo apt install rclone -y # Debian/Ubuntu
# sudo yum install rclone -y # CentOS/RHEL
# 交互式配置(选择你的存储提供商)
rclone config
# 支持的提供商包括:
# - Backblaze B2(最便宜,$0.006/GB/月)
# - Wasabi($0.0069/GB/月,无出站费)
# - AWS S3 / Cloudflare R2 / 阿里云OSS / 腾讯COS
# - Google Drive / OneDrive 等个人云盘
配置完成后,我们来写一个上传脚本
1 | /usr/local/bin/sync_remote.sh |
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 #!/bin/bash
# 远程同步脚本 - 在本地备份完成后调用
set -euo pipefail
BACKUP_ROOT="/var/backups"
RCLONE_REMOTE="backup-vps" # rclone remote 名称
RCLONE_PATH="vps-backups" # 远程存储路径
ENCRYPT_PASSWORD="${ENCRYPT_PASSWORD:-}" # 加密密码(可选)
# 查找最新备份目录
LATEST=$(ls -td "${BACKUP_ROOT}"/backup_* 2>/dev/null | head -1)
if [ -z "$LATEST" ]; then
echo "[ERROR] 没有找到备份目录"
exit 1
fi
echo "[INFO] 同步 ${LATEST} 到远程存储..."
if [ -n "$ENCRYPT_PASSWORD" ]; then
# 加密后上传(推荐)
tar czf - -C "$LATEST" . | \
openssl enc -aes-256-cbc -salt -pass "pass:${ENCRYPT_PASSWORD}" | \
rclone rcat "${RCLONE_REMOTE}:${RCLONE_PATH}/$(basename ${LATEST}).enc.tar.gz"
echo "[OK] 加密上传完成"
else
# 直接上传(不加密,适合信任的存储商)
rclone copy "$LATEST" "${RCLONE_REMOTE}:${RCLONE_PATH}/$(basename ${LATEST})" \
--progress --transfers=4 --checkers=8
echo "[OK] 直接上传完成"
fi
# 清理远程过期备份(保留30天)
rclone delete "${RCLONE_REMOTE}:${RCLONE_PATH}" \
--min-age 30d --verbose 2>&1 | grep -v "Couldn't find"
echo "[OK] 远程过期备份已清理"
1
2
3 chmod +x /usr/local/bin/sync_remote.sh
# 测试上传
sudo /usr/local/bin/sync_remote.sh
第三步:自动备份定时任务
手动运行备份不是长久之计,我们用 systemd timer 来替代传统的cron——更加可靠,且支持日志记录、依赖管理和失败通知。
创建 systemd service
创建
1 | /etc/systemd/system/vps-backup.service |
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 [Unit]
Description=VPS Daily Backup Service
Documentation=https://tbr8.org/vps-backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStartPost=/usr/local/bin/sync_remote.sh
StandardOutput=journal
StandardError=journal
# 超时保护:超过2小时的备份任务自动终止
TimeoutStartSec=7200
# OOM保护
OOMScoreAdjust=-500
创建 systemd timer
创建
1 | /etc/systemd/system/vps-backup.timer |
:
1
2
3
4
5
6
7
8
9
10
11
12 [Unit]
Description=Daily VPS Backup Timer
Requires=vps-backup.service
[Timer]
# 每天凌晨3点执行(避开业务高峰期)
OnCalendar=daily
FixedRandomDelay=30m
Persistent=true
[Install]
WantedBy=timers.target
启用定时器:
1
2
3
4
5
6
7
8
9
10
11
12
13 sudo systemctl daemon-reload
sudo systemctl enable vps-backup.timer
sudo systemctl start vps-backup.timer
# 检查状态
sudo systemctl status vps-backup.timer
# 查看下次执行时间
sudo systemctl list-timers vps-backup.timer
# 手动触发测试
sudo systemctl start vps-backup.service
# 查看执行日志
sudo journalctl -u vps-backup.service -f
第四步:灾难恢复手册
备份的价值只有在恢复时才能体现。这里我们准备一份完整的灾难恢复流程,建议打印出来贴在工位上。
场景一:数据库损坏
1
2
3
4
5
6
7
8
9 # 从本地恢复
gunzip < /var/backups/backup_20260711/mysql_mydb.sql.gz | mysql -u root -p mydb
# 从远程加密备份恢复
rclone cat backup-vps:vps-backups/backup_20260711.enc.tar.gz | \
openssl enc -d -aes-256-cbc -salt -pass "pass:${PASSWORD}" | \
tar xz -C /tmp/restore
# 然后导入
gunzip < /tmp/restore/mysql_mydb.sql.gz | mysql -u root -p mydb
场景二:整机迁移
当需要迁移到新的VPS时,完整流程如下:
- 在新VPS上安装相同版本的操作系统和软件栈(Nginx/MySQL/PHP等)
- 在新VPS上安装rclone并配置相同的remote
- 拉取最新的加密备份:
1rclone copy backup-vps:vps-backups/backup_latest.enc.tar.gz ./
- 解密并解压到临时目录
- 按需恢复数据库、网站文件、配置文件
- 修改DNS记录指向新VPS的IP
- 验证服务正常运行后,旧VPS保留一周再下线
场景三:文件被勒索加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 # 1. 立即隔离受感染VPS
sudo systemctl stop nginx php-fpm mysql
# 2. 从远程备份恢复(不连接本地,避免二次感染)
rclone copy backup-vps:vps-backups/backup_20260710.enc.tar.gz /tmp/recovery/
cd /tmp/recovery
openssl enc -d -aes-256-cbc -salt -pass "pass:${PASSWORD}" \
-in backup_20260710.enc.tar.gz | tar xz
# 3. 恢复到干净目录
cp -a /tmp/recovery/files_var_www.tar.gz /var/
cd /var && tar xzf files_var_www.tar.gz
# 4. 恢复数据库
gunzip < /tmp/recovery/mysql_mydb.sql.gz | mysql -u root -p mydb
# 5. 重启服务并检查
sudo systemctl start mysql nginx php-fpm
第五步:监控与告警
自动备份如果没有监控,就等于没有备份——你可能直到需要恢复时才发现已经连续两周备份失败了。
集成告警通知
在备份脚本末尾添加通知逻辑,使用
1 | curl |
发送到多个渠道:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 # 在 backup.sh 末尾添加
notify_success() {
local msg="[VPS备份成功] $(hostname) | 大小: $(du -sh ${BACKUP_DIR} | cut -f1) | 时间: $(date)"
# Telegram Bot 通知
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}&text=${msg}" &>/dev/null
# 钉钉/企业微信 Webhook
curl -s -X POST "${DINGTALK_WEBHOOK}" \
-H "Content-Type: application/json" \
-d "{"msgtype":"text","text":{"content":"${msg}"}}" &>/dev/null
}
notify_failure() {
local err_msg="[VPS备份失败] $(hostname) | 错误: $1"
# 同上渠道发送告警
# 建议同时发送邮件或用 Pushover/Slack
}
备份健康检查脚本
创建一个独立的健康检查脚本
1 | /usr/local/bin/check_backup_health.sh |
,每天在备份后运行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 #!/bin/bash
# 备份健康检查
BACKUP_ROOT="/var/backups"
WARN_IF_OLDER_THAN=28 # 小时
LATEST_BACKUP=$(ls -td ${BACKUP_ROOT}/backup_* 2>/dev/null | head -1)
if [ -z "$LATEST_BACKUP" ]; then
echo "CRITICAL: 没有任何备份存在!"
exit 2
fi
BACKUP_TIME=$(stat -c %Y "$LATEST_BACKUP")
CURRENT_TIME=$(date +%s)
AGE_HOURS=$(( (CURRENT_TIME - BACKUP_TIME) / 3600 ))
if [ $AGE_HOURS -gt $WARN_IF_OLDER_THAN ]; then
echo "WARNING: 最新备份已超过 ${AGE_HOURS} 小时"
exit 1
fi
BACKUP_SIZE=$(du -sb "$LATEST_BACKUP" | cut -f1)
if [ "$BACKUP_SIZE" -lt 1024 ]; then # 小于1KB视为异常
echo "CRITICAL: 备份大小异常(仅 ${BACKUP_SIZE} 字节)"
exit 2
fi
echo "OK: 备份正常,${AGE_HOURS}小时前创建,大小: $(numfmt --to=iec $BACKUP_SIZE)"
exit 0
进阶:数据库实时备份(Binlog)
对于关键业务数据库,每天一次的备份可能还不够。我们可以开启MySQL的binlog来实现任意时间点恢复(PITR)。
开启Binlog
编辑
1 | /etc/mysql/mysql.conf.d/mysqld.cnf |
(或
1 | /etc/my.cnf |
):
1
2
3
4
5
6 [mysqld]
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
expire_logs_days = 7
max_binlog_size = 100M
server-id = 1
重启MySQL后,我们可以结合全量备份和binlog实现任意时间点恢复:
1
2
3
4
5
6
7
8
9
10 # 1. 先恢复最近的完整备份
mysql -u root -p mydb < full_backup.sql
# 2. 应用binlog到指定时间点
mysqlbinlog --stop-datetime="2026-07-11 14:30:00" \
/var/log/mysql/mysql-bin.000045 | mysql -u root -p mydb
# 3. 或者恢复到指定位置
mysqlbinlog --stop-position=1234567 \
/var/log/mysql/mysql-bin.000045 | mysql -u root -p mydb
总结
一套完善的VPS备份体系应该包含以下要素:
- 自动化:定时任务自动执行,无需人工操作
- 分层存储:本地快照 + 远程加密存储 + 异地容灾
- 监控告警:备份失败必须能第一时间通知到人
- 定期演练:每月至少做一次恢复测试,确保备份可用
- 文档化:把恢复流程写成文档,紧急时照做即可
最后分享一个血泪教训:没有经过恢复测试的备份等于没有备份。很多人配置好备份后就再也不管了,直到某天需要恢复才发现备份文件损坏、加密密码丢失、存储空间已满——请务必每月做一次完整的恢复演练。
如果你按照本文的步骤完成配置,那么你的VPS数据安全等级已经超过了95%的同类用户。数据无价,备份无价。
汤不热吧