故事是从一封邮件开始的。
6 月 12 号,收到 DigitalOcean 的通知:他们要退出 GitHub 学生开发者包计划,所有通过学生包发放的积分,统一在 7 月 31 号作废。不是我账户的问题,也不是「激活满一年自然到期」——我今年三四月才激活,按原政策能用到 2027 年春天。是 DO 单方面终止,强制所有人一刀切。
我账上还剩约 $169 积分,跑着一台 $24/月的 4GB Droplet。算了下,到期前顶多再烧掉 $38,剩下 ~$130 会直接打水漂,不可转移、不可顺延。更现实的问题是:7 月 31 号之后账户转标准计费,跑着的资源会照着绑定的卡扣钱。
所以这不是「要不要搬」的问题,是「什么时候搬、搬去哪」的问题。还剩约 49 天。
先盘清楚要搬什么
动手之前,我把这台机器上跑的东西全列了一遍:
- Vaultwarden —— 自建密码库,最高优先级,数据必须完整无损
- code-server —— 浏览器里的开发环境,日常主力
- Uptime Kuma / Homepage / Dockge —— 监控、导航页、容器面板
- 一个 bili 学习进度追踪的 Node 服务(pm2 托管)
- cloudflared —— Cloudflare Tunnel,所有域名的入口
- 三条 cron:证书续期、bili 备份、某签到脚本
好消息是:这些全是标准 Linux 栈,没有任何 DO 平台锁定,理论上可以整体搬走。
选机:穷尽了学生包,发现没有平替
第一反应是「学生包里还有没有别的云主机额度能顶上」。于是把包里 60 多项福利全核对了一遍:
- Azure 给学生 $100 + 一堆免费服务,是唯一的真 VM,但 $100 大概只够 4GB 规格跑 3 个月。
- Heroku 给 $13/月 × 24 个月,但它是 PaaS,没 root、没自由端口,只能跑 Web 应用。
- Codespaces、Appwrite 之类要么会休眠、要么是 BaaS,都不能当 7×24 的服务器。
结论很干脆:学生包里没有任何能 1:1 替代 DO 的 VPS 额度,DO 本来就是包里独一档的福利。
那就自己掏钱。最后定的方案是:一台 Hetzner,把全部负载装进去。同样 4GB 规格,Hetzner 一个月 €4.49,DO 要 $24,差了大概 5 倍。
唯一要权衡的是地区——便宜的机房在欧洲,中国过去延迟 200–300ms。但我想清楚了一件事:这台机器的主力是 code-server 这类开发服务,它们全走 Cloudflare 隧道,用户连的是 CF 的亚洲边缘节点,欧洲源站那点高延迟会被 CF 边缘吸收掉,体感只是「可用、略慢」。能接受。国内 VPS 则直接排除——要备案、本身翻不出墙、而且很多用途牵连实名,不干净。
备份:先有退路,再动手
我给自己定的铁律是:任何破坏性操作之前,先有一份能恢复的备份。
备份这一步踩到了第一个坑,而且是个很隐蔽的坑。我一直以为所有 Docker 服务的数据都在 self-hosted/ 那棵目录树下。结果 docker inspect 实测发现,数据分在两棵树里:Uptime Kuma、Dockge、各栈的 compose 文件在 self-hosted/,但 Vaultwarden 的密码库和 Homepage 的配置,在另一个独立的 /opt/stacks/ 下。
这个坑要命在哪?如果我只备份 self-hosted/,就会丢掉整个密码库——而那正是优先级最高、最不能丢的东西。靠纸面记忆是发现不了的,只有实际去 inspect 每个容器的挂载点才看得到。
确认完整范围后,把状态数据、系统配置、个人文件、Claude 的记忆四组打包,用 GPG 做 AES256 加密,再用 rclone 传到 Google Drive 做异地冗余。加密口令我没让任何自动化流程碰——亲手敲进去,让它永远不落进任何命令历史或文档。最后下载回来解密验证了一遍,确认能还原,才敢往下走。
迁移:最易的和最关键的
真正搬的时候,发现各服务的难度差得很远。
最容易的是 cloudflared。 它是「token 托管隧道」——所有 ingress 规则和 DNS 都在 Cloudflare 面板上,token 内嵌在 systemd 单元里。意味着新机器装上 cloudflared、用同一个 token 重新跑,域名和 DNS 一行都不用改,自动跟随。这是整次迁移最大的利好。
唯一要小心的是:同一个 token 在两台机器同时跑,CF 会在两个连接之间做负载均衡,流量可能命中那台还没准备好的新机。所以测试期我用独立的测试域名,等新机所有服务都就绪了,才正式切换。
最关键的是 Vaultwarden。 策略是单独先迁它、先验证能登录解密整个密码库,确认无误,再去碰别的。数据本身只有 400 多 KB,但它是命脉,值得单独走一遍流程。
其余的就是体力活:Dockge 把 stack 放回去就自动接管;bili 服务重装依赖、还原数据库;恢复三条 cron。整个并行期 DO 那台原封不动,新机任何一步出问题都能随时退回去继续用旧机——这种零成本的回退网,是有积分没作废、两台并行的窗口期才有的,必须用足。
路上踩的几个坑
搬家很少有一帆风顺的,这次也踩了几脚:
1. better-sqlite3 编译不兼容。 bili 服务用的 better-sqlite3 是在旧机的 Node 18 上编译的原生模块,新机 Node 22 直接加载失败。npm rebuild 重新编译解决。原生依赖跨 Node 大版本基本都得重编,这个早该想到。
2. 误删了 Docker stack 数据。 这是最惊险的一次。克隆 self-hosted 的 git 仓库前,我没先检查目标目录是否已存在,一个 rm -rf 把已经放进去的 Docker stack 数据删掉了。还好——就是「还好」前面有备份——从 Google Drive 的加密包里重新恢复了一份。这一脚正好印证了前面那条铁律的价值:先有退路再动手,不是仪式,是真能救命。
3. 监控误报。 Uptime Kuma 里对 bili 的健康检查指向了根路径,新机起来后一直 404 报红。改成探测 /api/ping(返回 200)才正常。
4. SQLite 的 WAL 三件套。 SQLite 开了 WAL 模式后,数据其实分散在 .db、.db-wal、.db-shm 三个文件里,最新的写入可能还压在 WAL 里没合并进主库。所以还原时必须三件套一起搬,只拷主库会丢掉最近的数据。
收尾:实测推翻纸面,和一个潜伏 39 天的炸弹
服务全部上线后,我没有直接宣布完工,而是做了一轮实测收尾审查——不是对着文档打勾,是去旧机上实际跑命令核对。
这一审就审出问题了。我文档里记着旧机的隧道路由有四条,实际 cloudflared 的 ingress 配置里只有两条。纸面记忆是错的,幸好关机前核对了,否则按错的清单去清理 DNS 会出岔子。
更吓人的是 bili 的备份脚本。审查发现它已经静默失效 39 天了,而且是双重损坏:
- cron 那行没传数据路径参数,脚本里的
[ -f "$SRC" ]判断恒为假,于是每天都安静地 exit 0,不报任何错; - 就算路径对了,它用
cp data.db备份,不含 WAL 文件,备出来的还是个不完整的库。
铁证是备份目录里最后一个成功的文件停在 39 天前。如果新机也照搬这套 cron,那就是上线即零备份,丢库时毫无征兆、无法恢复。这种「不报错的失败」比直接崩掉危险得多——崩了你至少知道。当即在新机修了脚本和 cron。
最后留了一份关 DO 前的清单:先轮换所有可能在迁移过程中暴露过的密钥(隧道 token、各服务凭证),再删旧隧道、关 Droplet。在新机观察几天确认一切稳定后,再正式退役旧机——把那 $130 在作废之前的最后价值,榨成了一段从容的验证窗口。
几点回过头的总结
- 备份不是流程仪式,是真能救命的退路。 这次要不是先有加密备份,一个
rm -rf就够喝一壶了。 - 实测永远大于纸面。 数据在哪棵目录树、隧道有几条路由、备份到底有没有在跑——靠记忆和文档都会骗你,只有去机器上实际查才作数。
- 最该怕的是静默失败。 那个跑了 39 天、每天「成功」exit 0 的备份脚本,比任何一次明晃晃的报错都危险。
- 有并行窗口就用足。 旧机积分没到期、两台一起跑的那几天是零成本的安全网,让我可以放心大胆地试、错了就退。
被一封通知邮件逼着搬了一次家,结果意外地把这台跑了大半年、积了不少灰的服务器从里到外重新审了一遍。有时候外力推一把,反而能逼你把欠的债还清。