Linux VPS 最稳妥的备份方式是:每天用 restic 把加密快照发送到服务器之外的存储,设定清晰的保留策略,并定期测试恢复。本文在 Debian 12/13、Ubuntu 22.04/24.04 和 Rocky Linux / AlmaLinux 上完成整套配置:安装 restic、选择异地仓库(S3 兼容存储、SFTP 或 rclone)、导出数据库、用 systemd 定时器自动运行,并验证数据确实能恢复。
为什么备份必须放在 VPS 之外
放在同一台服务器上的副本不算备份:任何删除服务器的操作都会把副本一起删掉。在 JUSTG 云服务器上,以下几种正常操作都会永久删除数据:
- 更换机房(Change location):系统会新装一台 VPS,分配新 IP,不复制任何数据。新 VPS 就绪后,旧 VPS 及其全部数据会被删除。
- 逾期未续费:未付款的服务会在到期日自动暂停;到期 7 天后账单仍未支付,服务会被取消,数据随之删除。
- 重装系统会清空硬盘;选择 Immediate 方式取消服务,请求处理后服务和数据会立即删除。
再加上误敲的 rm -rf、失败的升级或服务器被入侵,结论很简单:唯一可靠的备份,是你自己做、并存放在别处的备份。在更换机房、重装系统或取消服务之前,请先手动运行一次备份,并确认新快照已经生成。
步骤 1:安装 restic
Debian 12/13、Ubuntu 24.04 和 EPEL 自带的版本已经足够新:
# Debian 12/13, Ubuntu 24.04
apt update && apt install -y restic
restic version
# Rocky Linux / AlmaLinux 8, 9 (from EPEL)
dnf install -y epel-release && dnf install -y restic
Ubuntu 22.04 自带的是 restic 0.12,不支持压缩,也无法使用当前的仓库格式。在这个系统上,请安装官方二进制文件并校验 checksum:
# Official static binary (Ubuntu 22.04 or any distro with an old restic)
VER=0.18.1 # check https://github.com/restic/restic/releases for the latest version
cd /tmp
curl -fLO https://github.com/restic/restic/releases/download/v${VER}/restic_${VER}_linux_amd64.bz2
curl -fLO https://github.com/restic/restic/releases/download/v${VER}/SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS # must print: OK
bunzip2 restic_${VER}_linux_amd64.bz2
install -m 0755 restic_${VER}_linux_amd64 /usr/local/bin/restic
restic version
步骤 2:选择异地仓库
仓库就是存放加密快照的地方。任选一种方案,保存为 /etc/restic/env(每行一个 NAME=value)。
方案 A:S3 兼容对象存储
任何提供 S3 API 的服务商都可以。创建一个存储桶和只限该存储桶的访问密钥,并在 URL 中填写服务商的 endpoint:
# /etc/restic/env (S3-compatible object storage)
RESTIC_REPOSITORY=s3:https://s3.example.com/example-backups/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=REPLACE_WITH_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=REPLACE_WITH_SECRET_KEY
# Optional: monitoring ping URL (Step 11)
HC_PING_URL=
方案 B:通过 SFTP 备份到另一台服务器
使用你自己的另一台服务器,最好位于不同机房或不同服务商,并建立专用的 backup 用户,由它拥有 /srv/restic:
# /etc/restic/env (SFTP to a second server)
RESTIC_REPOSITORY=sftp:[email protected]:/srv/restic/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
HC_PING_URL=
# One-time SSH key login for root on the VPS
ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519 # skip if the key exists
ssh-copy-id [email protected]
ssh [email protected] true # accept the host key once
方案 C:rclone 后端
rclone 能让 restic 连接几十种存储服务(Backblaze B2、Google Drive、OneDrive、WebDAV 等):
# Install and configure rclone (interactive wizard, run as root)
apt install -y rclone # Rocky/Alma: dnf install -y rclone (EPEL)
rclone config # create a remote, for example named "offsite"
# /etc/restic/env (any rclone backend)
RESTIC_REPOSITORY=rclone:offsite:example-backups/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
RCLONE_CONFIG=/root/.config/rclone/rclone.conf
HC_PING_URL=
/etc/restic/env 里的凭据通常也有删除备份的权限,攻击者一旦控制 VPS,就可能清空仓库。想要更强的保护,可以为存储桶开启版本控制或 object lock,或使用只能追加写入的目标,并且不要把存储账号的登录密码放在服务器上。步骤 3:创建密码文件并初始化仓库
install -d -m 0700 /etc/restic
openssl rand -base64 32 > /etc/restic/password
chmod 600 /etc/restic/password
nano /etc/restic/env # paste one of the examples above
chmod 600 /etc/restic/env
# Load the settings into the current shell and create the repository
set -a; . /etc/restic/env; set +a
restic init
/etc/restic/password 和 /etc/restic/env 复制到密码管理器或服务器以外的安全位置:VPS 一旦不在了,恢复只需要这两个文件。步骤 4:确定备份哪些内容、排除哪些内容
只备份配置和数据,不必备份整个操作系统:重装系统比恢复系统文件更快,快照也更小。
# /etc/restic/include - one path per line; delete lines that do not exist
/etc
/root
/home
/srv
/var/www
/opt
/usr/local
/var/spool/cron
/var/backups/db
# /etc/restic/excludes
/root/.cache
/home/*/.cache
**/node_modules
**/*.tmp
/var/www/*/cache
- 需要备份:
/etc、用户主目录、网站根目录、应用数据、crontab 以及数据库导出目录。 - 无需备份:
/proc、/sys、/dev、/run、/tmp、swap 文件和各类缓存。它们不在列表里,restic 根本不会读取。 - 数据库原始文件(
/var/lib/mysql、/var/lib/postgresql)在数据库运行时直接复制,可能得到不一致的副本。请备份导出文件(步骤 5)。 - Docker:不要整体备份
/var/lib/docker,应备份 compose 文件以及存放数据的卷或目录。
步骤 5:每次备份前先导出数据库
下面的脚本把压缩后的导出文件写入 /var/backups/db,该目录已在备份列表中。请按服务器实际情况设置 MYSQL 和 POSTGRES:
#!/bin/bash
# /usr/local/sbin/db-dump.sh - consistent database dumps before each backup
set -euo pipefail
umask 077
MYSQL=yes # yes / no
POSTGRES=no # yes / no
OUT=/var/backups/db
mkdir -p "$OUT"
if [ "$MYSQL" = yes ]; then
DUMP=$(command -v mariadb-dump || command -v mysqldump)
"$DUMP" --all-databases --single-transaction --quick \
--routines --events --triggers | gzip > "$OUT/mysql-all.sql.gz.tmp"
mv "$OUT/mysql-all.sql.gz.tmp" "$OUT/mysql-all.sql.gz"
fi
if [ "$POSTGRES" = yes ]; then
cd /tmp
runuser -u postgres -- pg_dumpall | gzip > "$OUT/postgres-all.sql.gz.tmp"
mv "$OUT/postgres-all.sql.gz.tmp" "$OUT/postgres-all.sql.gz"
fi
--single-transaction 能在不锁住网站的情况下得到一致的 InnoDB 导出。在 Debian 和 Ubuntu 上,root 通过本地 socket 连接 MariaDB/MySQL,无需密码;如果你的服务器需要密码,请写入 /root/.my.cnf(权限 600),不要写在命令行里。
步骤 6:创建备份脚本
一个脚本完成数据库导出、备份、按策略清理和每周检查,并向可选的监控 URL 汇报结果(步骤 10)。任何一步失败都会让脚本报错退出,问题不会被悄悄忽略:
#!/bin/bash
# /usr/local/sbin/restic-backup.sh - dump databases, back up, apply retention
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
set -a; . /etc/restic/env; set +a
fail() {
[ -n "${HC_PING_URL:-}" ] && curl -fsS -m 10 --retry 3 "$HC_PING_URL/fail" >/dev/null || true
}
trap fail ERR
/usr/local/sbin/db-dump.sh
restic backup \
--files-from /etc/restic/include \
--exclude-file /etc/restic/excludes \
--exclude-caches \
--tag daily
restic forget --prune \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6
# Sundays: verify the repository and read back 5% of the data
if [ "$(date +%u)" = 7 ]; then
restic check --read-data-subset=5%
fi
if [ -n "${HC_PING_URL:-}" ]; then
curl -fsS -m 10 --retry 3 "$HC_PING_URL" >/dev/null
fi
为脚本加上执行权限,并手动运行第一次备份。首次运行会上传全部数据,之后只上传变化部分。
chmod 700 /usr/local/sbin/db-dump.sh /usr/local/sbin/restic-backup.sh
/usr/local/sbin/restic-backup.sh # first run by hand, watch for errors
restic snapshots
步骤 7:用 systemd 定时器(或 cron)定时运行
推荐使用 systemd 定时器:日志会写入 journal,错过的任务会在开机后补跑(Persistent=true),还能降低任务的 CPU 和磁盘优先级,不拖慢网站。
# /etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup to off-site repository
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-backup.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=30min
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer
# Test the service once and read its log
systemctl start restic-backup.service
journalctl -u restic-backup.service -n 50 --no-pager
如果你更习惯 cron,可以用下面这行代替定时器(二者只选其一):
# /etc/cron.d/restic-backup (only if you do not use the systemd timer)
30 3 * * * root /usr/local/sbin/restic-backup.sh >> /var/log/restic-backup.log 2>&1
步骤 8:用 restic forget --prune 设置保留策略
restic forget 决定保留哪些快照,--prune 删除已不被任何快照引用的数据。脚本中的策略会保留:
| 参数 | 保留内容 |
|---|---|
--keep-daily 7 | 最近 7 天中每天最后一个快照 |
--keep-weekly 4 | 最近 4 周每周一个快照 |
--keep-monthly 6 | 最近 6 个月每月一个快照 |
启用新策略之前,先用 --dry-run 预览效果:
set -a; . /etc/restic/env; set +a
restic snapshots
# Preview what a policy would remove, without deleting anything
restic forget --dry-run --keep-daily 7 --keep-weekly 4 --keep-monthly 6
restic stats --mode raw-data
脚本还会在每周日运行 restic check,检查仓库结构;--read-data-subset=5% 每次下载并校验不同的 5% 数据,让损坏在你真正需要备份之前就被发现。需要更放心时,也可以随时手动运行 restic check。
步骤 9:测试恢复
从未恢复过的备份只是一种希望。每月一次,把真实数据恢复到临时目录并进行比对:
set -a; . /etc/restic/env; set +a
restic snapshots
restic ls latest /etc/nginx
# Restore one directory to a scratch location, then compare
restic restore latest --target /tmp/restore-test --include /etc/nginx
diff -r /etc/nginx /tmp/restore-test/etc/nginx && echo "restore OK"
# Check that the database dump is complete (last line: "-- Dump completed on ...")
restic dump latest /var/backups/db/mysql-all.sql.gz | zcat | tail -n 1
rm -rf /tmp/restore-test
最坏的情况也要演练一次,例如在一台备用 VPS 上:一台全新服务器,手里只有保存好的那两个文件。
# On the NEW server: install restic, then restore the two saved files
install -d -m 0700 /etc/restic
nano /etc/restic/password && nano /etc/restic/env
set -a; . /etc/restic/env; set +a
restic restore latest --target /srv/restore
rsync -a /srv/restore/var/www/ /var/www/ # copy back what you need
zcat /srv/restore/var/backups/db/mysql-all.sql.gz | mariadb # or: mysql
restic restore latest --target /:它会覆盖正在使用的文件,包括可能不适合新服务器的系统配置。请恢复到单独的目录,再把需要的内容复制回去。步骤 10:备份失败时及时收到提醒
备份往往是悄无声息地失败的:密钥过期、存储桶满了、密码被改了。“dead man's switch”类服务可以解决这个问题:脚本每次成功后访问一个 URL,若没有按时收到访问,就会向你发出提醒。你可以自建(例如开源的 Healthchecks),也可以使用任何类似服务,然后把检查 URL 写入 /etc/restic/env:
HC_PING_URL=https://hc.example.com/ping/REPLACE-WITH-YOUR-CHECK-ID
脚本成功时访问该 URL,出错时访问 $HC_PING_URL/fail。把检查周期设为一天,并留出几个小时的宽限时间。如果不用这类服务,至少每周查看一次 restic snapshots 和 journalctl -u restic-backup.service。
FAQ
需要多大的存储空间?
首次快照大约等于你的数据量(压缩后通常更小),之后每天只增加少量变化数据。运行几周后执行 restic stats --mode raw-data,即可看到实际用量。
restic 提示仓库被锁定,怎么办?
说明上一次运行被中断,留下了过期的锁。先确认没有 restic 进程在运行(pgrep -a restic),再执行 restic unlock,然后重新运行备份。
在 512 MB 方案上 restic 占用内存太多,能降低吗?
可以在 /etc/restic/env 中加入 GOGC=20,让 restic 更频繁地释放内存;同时精简备份路径,并确认 VPS 已配置 swap。如果内存仍然不够,可以在服务页面升级方案。需要帮助请提交工单:JUSTG 提供 7×24 小时 AI 客服(工单),需要人工处理的问题会由工程师跟进。