我的个人站点跑在一台海外的云服务器上:博客、工作台、自动发布流水线都在上面。服务器每天会自己生成一个备份包,但备份包只躺在同一台服务器上是不够的:服务器整个坏掉、被误删,或者被人拿下,备份会跟着一起没。
所以备份得离开服务器。常见做法是服务器定时往外推,推到对象存储或另一台机器。我反过来做:由家里的 Mac 每 3 小时通过 SSH 去服务器上拉。这篇讲为什么选拉取、拉取这一侧要注意什么,以及一件我还没做、但必须做的事。
一、为什么是拉,不是推
关键在于谁手里拿着钥匙。
- 推送:服务器上要存一份能写入备份目的地的凭据。服务器一旦被拿下,攻击者也就拿到了这份凭据,可以顺手把备份删掉或改掉。勒索类攻击最喜欢先清备份。
- 拉取:服务器上只有一把“允许某台机器来读”的公钥,它对备份的存放地点一无所知。钥匙在家里,服务器出事,家里的副本碰不到。
还有两个顺带的好处:家里的电脑在路由器后面,不需要对外开任何端口;而且备份直接落在我自己的硬盘上,不依赖任何云存储账号。
代价也很明显:家里的电脑得开着。所以拉取频率定成每 3 小时一次,而不是每天一次,只要白天或晚上有一段时间开机,当天的备份就能拉到。
二、拉取那把钥匙,权限要压到最小
拉取用的 SSH 私钥只存在那台 Mac 上,放在备份脚本自己的目录里。这把钥匙最好专钥专用,不和日常登录服务器的钥匙混用。服务器这一侧,对应的公钥最好再加几道限制,让它即使泄露也只能用来读备份:
# ~/.ssh/authorized_keys(备份专用用户)示意
restrict,command="/usr/local/bin/backup-serve" ssh-ed25519 AAAA... backup-pull
- 单独的系统用户,只对备份目录有读权限。
restrict关掉端口转发、终端分配这些用不到的功能。command=把这把钥匙锁死在一条命令上,比如只允许列出和读取备份文件,登录进来也开不了 shell。- 家里如果有固定出口 IP,还可以加
from=限定来源。
三、拉下来之后要检查
“文件拉下来了”不等于“备份可用”。拉取脚本在每次拉完之后至少该做三件事:
- 校验完整性:压缩包能完整解开(比如
gzip -t),大小没有离谱地变小。一个只有几 KB 的数据库导出,多半是导出失败了。 - 按日期保留:保留最近若干天的每日备份,再留几份每周、每月的,旧的按规则清掉。只留最新一份的话,问题发现得晚一点就全被覆盖了。
- 写日志,并且有人看:每次拉取写一行日志。更好的做法是“超过一天没拉到新备份就提醒”,接到手机推送里。备份悄悄停了几个月没人发现,是最常见的事故。
我现在的脚本做到了其中一部分:服务器每天凌晨 3:30 生成备份包,并附上 sha256;Mac 每 3 小时运行一次,有新包就拉下来校验,每次运行写一行日志。实际日志大概是这样:
04:01:23 已拉取 gokmfun-20261019-0330.tar.gz(27M,sha256 校验通过)
04:01:23 完成:新增 1,失败 0,本地共 22 份,最新 gokmfun-20261019-0330.tar.gz
07:01:26 完成:新增 0,失败 0,本地共 22 份,最新 gokmfun-20261019-0330.tar.gz
没有新包时也照样记一行,日志的时间戳本身就是“任务还活着”的证据。下一步是把“超过一天没有新包”接进手机推送,这样就不用靠我自己想起来翻日志。
四、还没做的事:恢复演练
坦白说,这套备份我还从来没有完整地恢复过一次。这意味着我现在拥有的其实是“备份文件”,还不能叫“能恢复的备份”。很多人的备份是在真正需要时才发现不能用:少了某个配置文件、数据库版本对不上、或者恢复步骤只在当初写脚本的人脑子里。
我给自己定的演练计划是这样的:
- 新开一台最便宜的临时云服务器,或者在本机起一组容器,不碰正在运行的站点。
- 只用家里拉下来的备份包,加上代码仓库,按文档一步步恢复。
- 检查文章、图片、评论、工作台数据是否都在,后台能否登录。
- 记下每一步花的时间,以及文档里缺了什么,补进文档。
- 删掉临时服务器。之后每隔几个月重复一次。
演练的产出不只是“确认能恢复”,更重要的是一份写清楚的恢复步骤。真出事时人往往很慌,照着做比现想靠谱得多。
小结
个人站点的备份,三件事最重要:备份要离开服务器,钥匙不放在服务器上,以及至少恢复过一次。前两件我用“家里拉取”做到了,第三件还欠着。写出来,也是给自己定一个期限。
