Skip to content

让家庭服务器按时睡觉、自动醒来:Mac 远程主机的生命周期管理

Published: at 12:008 min read
目录

上一篇文章《把 Mac 变成远程主机:家庭服务器的完整架构拆解》记录了 XindaRouter 的整体架构:Cloudflare Tunnel 负责公网入口,Colima/Lima 提供 Docker 运行时,Docker Compose 编排应用和数据服务,LaunchAgent 负责常驻。

那篇文章写的是“如何把服务跑起来”。真正运行一段时间后,我遇到的下一个问题是:这台 Mac 不需要整夜工作,但也不能每天早上靠我手动开机、解锁、启动一堆服务。于是系统又做了一次调整,目标从“能运行”变成了“能按时休息,也能自己恢复”。

这篇文章记录的不是一份通用安装教程,而是一套已经落地在 XindaRouter 上的生命周期设计。

先定义每天的节奏

现在的计划很简单:白天提供服务,晚上停止服务并让 Mac 睡眠,清晨定时唤醒后自动恢复。

21:30  生成本地备份,校验后加密并上传 R2
21:50  优雅停止容器和 Colima
22:00  macOS 进入睡眠
05:00  macOS 定时唤醒
05:00+ 健康检查发现服务离线,启动恢复流程

这里有一个容易混淆的区别:我不是让 Mac 在接电时因为空闲自动睡眠,而是关闭普通的自动睡眠,再用 pmset repeat 明确安排每天的睡眠和唤醒。这样,系统什么时候休息是可预测的,服务什么时候下线也有明确的窗口。

为什么选择睡眠,而不是关机

这台机器开启了 FileVault。普通睡眠唤醒后,用户会话和磁盘解锁状态仍然存在;而意外断电或关机后的冷启动,通常需要人在现场先解锁系统盘,用户级 LaunchAgent 才能继续工作。

因此,“关机再开机”并不能真正实现无人值守。它把最关键的一步留给了人工:解锁 FileVault、进入用户会话,然后等待服务启动。睡眠/唤醒则保留了磁盘加密,也避免了每天早上的人工启动流程。

这并不意味着睡眠没有代价。睡眠期间公网服务当然不可用,所以必须让监控知道这段时间是计划内停机;唤醒后也不能假设虚拟机和容器一定处于干净状态,需要额外的健康检查和恢复逻辑。

睡眠前,先把服务有序地停下来

每天 21:50 运行的停止脚本做三件事:记录状态、按依赖顺序停止服务、最后停止 Colima。

停止顺序是从客户端到数据层:先停止开发环境、Open WebUI、code-server、监控和 Gitea,再停止 XindaRouter 主应用,最后停止 PostgreSQL、Redis 和 Colima。数据库不是第一个被杀掉的进程,给应用留出正常退出和写入持久化数据的时间。

脚本会先写入一个不包含凭据的状态文件:

~/.config/xindarouter/scheduled-stop

这个文件不是锁,也不是配置文件,它只表示“这次服务下线是计划内的”。停止过程和错误信息分别写入日志,下一次排查时可以区分“脚本没有执行”和“脚本执行了但某个容器停止失败”。

一个看似合理、实际会打架的 watchdog

服务栈有一个每五分钟运行一次的 watchdog。它平时检查:

curl -fsS --max-time 5 http://127.0.0.1:8080/health

如果健康接口不可用,watchdog 就会启动 Colima 和 Compose。这对意外故障很有用,但它会和定时停机发生冲突:

停止服务
  -> 健康检查发现 8080 不通
  -> watchdog 误以为发生故障
  -> Colima 和容器又被启动

所以 watchdog 必须理解时间和意图。当前脚本在 22:00 到 05:00 之间直接退出,不做恢复;它还会检查 scheduled-stop 标记。05:00 唤醒后的第一次检查会删除这个标记,然后重新开启自动恢复。这样,计划内停机和真正的故障就被分开了。

这是一条很通用的经验:自动修复系统不能只看“当前状态不正常”,还要知道这是不是人为安排的状态。

唤醒之后,恢复不是一条命令

Mac 在 05:00 唤醒后,watchdog 会再次检查健康接口。如果 Colima 没有运行,恢复流程会先处理可能遗留的虚拟机状态,再启动 Colima。

在一次不完整的关机或虚拟机异常之后,Lima 可能留下过期的 PID、socket 或磁盘锁。当前恢复流程会尝试执行强制停止和磁盘解锁,然后再启动 Colima。只有 Colima 正常后,才会运行 mac-start.sh,启动 Compose 服务、开发环境和其他辅助容器。

启动完成也不等于恢复成功。脚本会最多等待约一分钟,反复检查 /health,只有健康接口恢复才记录成功;超过重试次数仍不可用,则记录失败并交给监控继续通知。

这种设计比“执行启动命令后就算成功”可靠得多。进程存在、容器处于 running 状态,都不能证明应用已经可以接受请求。

用 pmset 安排系统级的睡眠和唤醒

系统级计划由 macOS 的 pmset 管理。当前使用的是每天睡眠、每天唤醒:

sudo pmset repeat wakeorpoweron MTWRFSU 05:00:00 sleep MTWRFSU 22:00:00
pmset -g sched

第一条命令需要管理员权限,第二条用于检查计划是否真的写入。设置后,应该确认输出中只有预期的 wakeorpoweron 和 sleep,不要遗留旧的 shutdown 计划,否则系统层和服务层会出现互相矛盾的行为。

接电时的普通自动睡眠则保持关闭,同时允许屏幕关闭。例如当前机器使用了下面这类设置:

sudo pmset -c sleep 0 disksleep 0 powernap 1 womp 1
pmset -g custom

这两组设置并不矛盾:第一组是明确的日程,第二组是防止系统因为空闲提前睡眠。实际使用中仍要观察外接电源、散热和合盖状态;笔记本长期合盖运行不能只靠配置文件推断稳定性。

备份也要进入生命周期

睡眠之前最重要的动作不是停止容器,而是完成备份。现在每天 21:30 先导出 PostgreSQL、打包应用数据和 Open WebUI 数据,生成 SHA-256 校验文件。校验通过后,备份会用 age 加密,再上传到私有 Cloudflare R2。

Mac 只保存加密公钥,不保存解密私钥。解密私钥和 R2 恢复凭据保存在独立的 Windows 恢复目录中;R2 桶不开放公共域名。这样,即使 Mac 的磁盘和本地备份一起损坏,也还有异机恢复路径。

保留策略按日、周、月分层,避免无限增长。更重要的是,上传成功不是恢复成功的证明。至少要定期下载一个加密对象,完成解密、校验和数据库 dump 可读性检查。备份系统真正的验收标准是“能恢复”,不是“目录里有文件”。

这套方案仍然解决不了什么

现在的系统可以处理普通的每日睡眠、唤醒和大部分服务级故障,但它不是一台高可用服务器。

这些边界反而是方案可解释的一部分。知道系统在哪些情况下不能自愈,比笼统地宣称“完全无人值守”更有用。

结语:远程主机也需要作息

把旧 Mac 变成远程主机,第一阶段是把服务放上去,第二阶段是给它设计作息。

Cloudflare Tunnel 解决了访问问题,Colima 和 Docker Compose 解决了运行时问题,LaunchAgent 解决了常驻问题;而睡眠计划、优雅停止、状态标记、健康检查和异地备份,解决的是“这套系统能不能长期自己运转”。

家庭服务器不一定要 24 小时满负荷工作。对我来说,更合理的目标是:白天它可靠地提供服务,夜间它按计划休息,早晨醒来后能证明自己已经恢复。如果恢复不了,至少能留下足够清晰的日志和一份可验证的备份。

这比让一台笔记本永远亮着,更接近一个真正可维护的远程主机。