有天凌晨三点,我做了个梦:梦见自己手一抖,把网站目录删了。惊醒之后第一个念头不是"还好是梦",而是:如果这是真的,我拿什么恢复?答案是:什么都没有。那天早上送完孩子,我哪儿也没去,直接坐进书房,把备份方案搭了起来。

家里有保险柜放重要文件,服务器也得有个"保险柜"。下面这套方案跑在我这台 2核2G 的机器上,简单、便宜、够用。

先想清楚:备份什么

服务器要备份,家里的证件文件更得备份——我差点因为证件误过事,那篇的教训比这篇还疼。

我的博客是 Hugo 静态站,没有数据库,要备份的东西很清楚:

  1. 网站源文件:/var/www/blog 整个目录,包括 markdown 原稿、hugo.toml、主题。原稿丢了,博客就只剩个空壳。
  2. Nginx 配置:/etc/nginx,特别是我手写的那些 server 块。重装系统后照着恢复,十分钟就能复活。
  3. 数据库:我这站没有,但脚本里留了 mysqldump 的位置。哪天加了个带数据库的服务,直接把那两行注释打开就行,不用重写脚本。

备份目标就一个原则:能让我从零恢复整站的东西,才值得备份。零零碎碎的日志、缓存,不在名单上。

备份脚本:tar 打包,按日期命名

脚本放在 /root/backup.sh,内容不复杂:

#!/bin/bash
BACKUP_DIR="/root/backups"
DATE=$(date +%Y%m%d)
mkdir -p "$BACKUP_DIR"

# 打包网站源文件
tar -czf "$BACKUP_DIR/blog-$DATE.tar.gz" -C /var/www blog

# 打包 Nginx 配置
tar -czf "$BACKUP_DIR/nginx-$DATE.tar.gz" -C /etc nginx

# 如果有数据库,把下面两行打开
# mysqldump -u root -p'你的密码' --all-databases | gzip > "$BACKUP_DIR/mysql-$DATE.sql.gz"

# 只保留最近 7 天的备份,旧的自动删
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -delete

echo "backup done: $DATE"

几个说明:tar -czf 打包加压缩,一行搞定;文件名带日期,blog-20261024.tar.gz 这种,一眼知道是哪天的;最后两行 find ... -delete 是自动清理,只留 7 天。40G 的硬盘经不起无节制堆备份,7 天对个人博客足够了——真出问题,你不会等到第八天才发现。

给脚本执行权限,手动跑一遍确认能用:

chmod +x /root/backup.sh
/root/backup.sh
ls -lh /root/backups/

看到 tar.gz 文件生成,大小合理,第一步就成了。

定时:cron 每天凌晨跑

crontab -e

加一行:

0 3 * * * /root/backup.sh >> /var/log/backup.log 2>&1

每天凌晨 3 点执行,日志写到 /var/log/backup.log。选凌晨 3 点是因为那会儿没访客(希望我自己也在睡觉)。cron 这东西跟家里的自动喷水定时器一个逻辑:设好就不用管,但偶尔要看一眼它是不是真在工作。

验证 cron 生效,最简单的办法是第二天早上看一眼:

ls -lh /root/backups/
tail /var/log/backup.log

cron 每天凌晨跑,但它要是哪天悄悄罢工了我也不知道,所以我后来给机器装了个会喊疼的监控,免得备份停了几个月才发现。

有新日期的文件,日志里有 “backup done”,说明定时器在干活。

异地:再存一份,别把鸡蛋放一个篮子里

备份放在本机,只能防"我手抖删文件";要是整台 VPS 挂了、服务商跑路了,本机备份跟着一起完蛋。所以每天备份完,再同步一份到别处。

我用的 rclone,把备份目录同步到网盘:

rclone sync /root/backups/ remote:blog-backups/ --min-age 1d

这行也可以塞进 cron,在本地备份完成后执行。--min-age 1d 是只同步一天内的新文件,省流量。至于是 Google Drive、Dropbox 还是另一台 VPS,看你手里有什么——关键是"异地"这两个字,介质不重要。

我现在的链条是:VPS 本地每天打包 → 保留 7 天 → 新备份同步到网盘。两层保险,每年多花 0 美元(网盘用的免费额度,博客备份就几十 M)。

脚本本身也别忘了备份。/root/backup.sh 哪天手抖删了,cron 里那行还在,但执行的脚本没了,等于定时器空转。我把脚本内容贴了一份在私人笔记里,恢复起来就是复制粘贴的事。备份脚本的备份,听起来像绕口令,做起来就一分钟。

服务器这套备份只是我 3-2-1 习惯的一半,个人文件那边的备份逻辑写在另一篇里,思路一模一样:别把鸡蛋放一个篮子里。

实战:我真恢复过一次

备份方案上线第三个月,真用上了。有次我手抖(对,就是梦里那个动作,现实版),rm -rf 多敲了一个路径,博客的静态资源目录没了半边。心跳停了一拍,然后想起:有备份。

恢复过程:

# 1. 找到最近的备份
ls -lt /root/backups/ | head
# 2. 解压覆盖回去
tar -xzf /root/backups/blog-20261024.tar.gz -C /var/www
# 3. 重新构建站点
hugo --minify

从发现到恢复,前后 12 分钟。那 12 分钟里我手是抖的,但心里不慌——这就是备份的意义:它买的不是数据,是镇定。没备份的那次(梦见删库之前),我连抖的资格都没有,只能祈祷。

恢复完我还做了件事:把这次事故记进了笔记,标题就叫"rm -rf 惨案"。运维的经验,都是拿事故换的,换完了不记,下次还得再换一次。

3-2-1 法则:给我的方案打个分

备份界有个 3-2-1 法则:3 份拷贝,2 种不同介质,1 份异地存放。拿我的方案对照一下:

要求我的方案得分
3 份拷贝本机 7 天滚动 + 网盘 1 份 + 原稿在 GitHub✅ 3 份
2 种介质VPS 硬盘 + 网盘✅ 2 种
1 份异地网盘(异地)✅ 达标

满分。不是我多专业,是 Hugo 静态站天生备份友好——原稿在 GitHub 就天然多了一份异地拷贝,纯属沾光。用动态站(WordPress 之类)的朋友,数据库那份异地备份要更上心,mysqldump 的定时任务别偷懒。

别忘了:备份要能恢复才算数

很多人(包括曾经的我)备份完就觉得万事大吉,从没验证过备份包能不能用。这就跟买了灭火器从没看过压力表一样,关键时刻可能掉链子。

我的做法:每季度找个时间,把最新的备份包解压到 /tmp 看一眼:

tar -tzf /root/backups/blog-20261024.tar.gz | head

tar -tzf 只列文件清单不解压,确认包是完整的、文件都在。花一分钟,买个安心。这个习惯我记在了日历上,跟"换空调滤芯"同一个季度提醒——中年人的运维,靠日历驱动。

一句话总结

备份方案不用复杂:tar 按日期打包网站和配置,cron 每天凌晨自动跑,只留 7 天,rclone 同步一份到异地。每季度解压验证一次备份包是好的。先让博客"丢得起",再谈别的折腾。