我的服务器现在是一部手机

查看原文 HN 讨论

文章摘要

seg6 这篇文章记录了他把一部 CMF Phone 1 改造成家用个人基础设施服务器的完整过程,最有价值的地方在于他老老实实写下了两次「馊主意」以及为什么放弃它们。

起点:他的个人服务原本跑在一台小型 Hetzner VPS 上——几个 web 应用、一个叫 Surf 的远程浏览器、Caddy 以及一堆配套服务。能用,但他不想一直付钱。真正让这个妥协难以忽视的是 Surf:便宜的共享实例在 Chrome 有真活干的时候明显吃不消,而独占 CPU 的机型每月贵到让「个人浏览器」变成一笔可疑的财务承诺。买新机器也不是好出路——DRAM 价格已经涨到离谱,这时候攒一台内存宽裕的新机箱时机太差。他看了二手迷你 PC,也想过把台式机在不用的时候当服务器,然后想起了抽屉里那部 CMF Phone 1:八个 ARM 核心、8 GB 内存、128 GB 闪存、Wi-Fi 6、5G 调制解调器,还自带电池后备——一颗对「躺在抽屉里」这件事严重大材小用的 SoC,而且钱早就付过了。

今天这部手机跑着 Surf 及其托管的 Chrome 实例、他的个人财务追踪器、一个屏幕共享服务,还有一批小型 web 应用。它们能扛住重启、从 Git 部署、在手机切换网络时依然可达——这台机器真正取代了那台 VPS。

第一个馊主意:替换掉 Android。 最干净的方案看起来是刷一个正常的 Linux 发行版。CMF Phone 1 有 postmarketOS 的移植,能启动,设备页面上的绿色勾多到足以让一个鲁莽的人乐观起来——他就成了那个鲁莽的人。他没太注意的是标着「broken」的那些:Wi-Fi、蓝牙、硬件加速,以及其他大部分让手机能当小服务器的东西。他最远只走到了 postmarketOS 开机画面加一块黑屏。这时候他既没有服务器也没有手机了。

恢复原厂 Nothing OS 变成了另一个支线任务:刷机工具需要 Windows,于是他在 QEMU 里装了 Windows,和 USB 直通与联发科驱动搏斗,眼看刷机工具卡死,最后只好换到一台真正的 Windows 机器上恢复出厂镜像。中间有一刻手机软砖只显示黑屏,他真心以为自己把一台好端端的设备变成了镇纸。

它活过来了,教训是:Android 已经为这堆硬件的每一个部件提供了能用的驱动——Wi-Fi、电源管理、电池、GPU、调制解调器,以及每一个古怪的厂商细节都已经能工作。为了一个更常规的用户空间而把这些全扔掉,是个错误的交易。他实际上不需要手机变成一台普通的 Linux 机器,他需要的是让它可靠地运行 Linux 应用,同时让 Android 继续做它擅长的硬件相关工作。

Termux 就是宿主操作系统。 第二次尝试保留原厂 Android,把 Termux 当宿主环境。Termux 提供 OpenSSH、runit、Caddy、Cloudflared、包管理以及足够正常的 Unix 工具链;Termux:Boot 在重启后拉起 supervisor 和 SSH;Tailscale 给手机一个稳定的私有地址,于是从 tailnet 上任何机器都能直接 ssh cmf

Termux 不是虚拟机——它的进程仍然跑在 Android 的 Linux 内核上,但它基于 Bionic 的用户空间跟普通 Debian 差别够大,现成的 Linux 应用镜像没法直接丢进去。而这个割裂反倒有用:Termux 可以保持为一个小巧的宿主控制平面,而每个应用自带它期望的 Linux 文件系统。

实际服务由 runit 托管。Android 的电池管理在它的本职工作上非常出色,但对一台假装自己是服务器的设备而言非常糟糕,所以他的 Ansible 构建还会施加一份 Android 主机配置:安装持久 wake lock、禁用轻度和深度 idle、把 Termux、Termux:Boot 和 Tailscale 从后台限制中豁免、关掉子进程限制器、阻止 Wi-Fi 挂起、把 Tailscale 配成 always-on VPN。

比任何单项设置更重要的是恢复链:Android 启动 → always-on VPN 拉起 Tailscale → Termux:Boot → runit → 常驻服务 → 本地与公网健康检查。手机可以在他没注意的情况下自行重启并恢复。他坦言这不是传统 Linux 服务器:没有 systemd,没有正常的 Docker 守护进程,也没必要假装有。但它是一个 Linux 内核加上一个相当能干的用户空间,而这就够了。

第二个馊主意:proot。 他大部分应用已经以 Linux ARM64 OCI 镜像的形式发布,proot-distro 让这些镜像在 Debian 下跑起来出奇地容易,完全不用改应用本身。PRoot 在用户空间拦截文件系统和进程操作,让一个普通的 Termux 进程相信自己住在一个 Debian 根文件系统里。它不是容器边界——所有东西仍共享 Android 的内核、网络命名空间和 Termux UID——但作为应用兼容层极其有用,因为它既不需要 root 也不需要特殊内核。

普通 web 服务这么跑一开始没问题,每个应用拿到一个经过校验的根文件系统、一个回环端口和一个 runit 服务,Caddy 直接跑在 Termux 里把主机名路由到这些端口。唯一的例外是对性能和延迟敏感的 Surf 浏览器负载:启动进程、打开库、遍历路径、读取浏览器配置、搬运抓取数据,全都要穿过 PRoot 的用户空间翻译层。CPU 是有的,但 Chrome 没法高效地够到它。于是他给手机 root 了——不是为了替换 Android,而是为了把同一个 Debian 文件系统正经挂载起来、用真正的 chroot 进去。runit 仍然从 Termux 侧掌管生命周期,配置仍来自同一处,应用数据仍住在 Termux 存储里,只是负载通过原生系统调用直达 Android 内核。「提升一点都不细微!」

这条路走通之后,把较小的常驻服务继续留在 PRoot 下就没什么意义了,现在它们也用同样方式运行。他的工作站把每个 ARM64 镜像解析到确切的 digest 并导出其文件系统,Ansible 在手机上校验并安装。一个小的 root 助手创建私有挂载命名空间、绑定所需路径、chroot 进文件系统、降权,然后启动原镜像的 entrypoint。手机上既不需要 Docker 也不需要编译器。他强调这些仍然是兼容环境而非安全边界。他还花了太长时间试图通过 VirGL 和 Android Vulkan 把 Debian 的图形栈桥接到手机的 Mali GPU,拿到了硬件合成的勾但同时得到花屏和更差的性能——最终无聊的软件渲染路径反而表现更好。

基础设施,不是一堆 shell 历史。 他不想要一台由一周后就会忘记的命令拼出来的「宠物服务器」,于是把整台主机搬进 Ansible 管理的状态:版本、服务定义、路由、电源设置、密钥和健康检查全住在一个私有仓库里。部署流程是:release 或 OCI 镜像 → 在 Git 中固定 checksum/digest → 通过 SSH 跑 Ansible → 手机上的带版本文件 → 原子的 current 符号链接 → runit 服务 → 本地健康检查 → 公网边缘检查。发布按 digest 或 checksum 固定,装进带版本的目录,前面挂一个原子 current 符号链接;校验或健康检查失败就停止部署,回滚就是把固定值改回去再 apply 一次;应用数据与发布分开存放。手动引导只有一次性的几步(装三个 Android 应用、root 手机、给 Termux 超级用户权限、授权 SSH),之后同一个仓库掌管其余一切,日常操作简化到 make phonemake phone-statusmake phone-edge-check。重复执行不会替换未变的运行时文件,也不会重启健康的常驻服务。更重要的是,如果这部手机挂了,另一部可 root 的 ARM64 手机能被带到同样的状态,而不需要重建 shell 历史。

密钥不存在手机上的 Git checkout 里——因为根本没有 checkout。Ansible Vault 的值加密存在基础设施仓库里,vault 密码通过让 1Password SSH agent 对一个固定挑战签名而派生,私钥留在 1Password 里,手机永远不需要接触它。部署时 Ansible 只把每个服务需要的运行时值渲染进 Termux 的私有存储。

把流量送到家庭宽带后面的一部手机。 他的家庭连接没有 VPS 那样的静态服务器配置,他也不想把 SSH 和一堆随机应用端口通过路由器暴露出去。而且他希望手机在一个重要意义上仍然是手机:应该能拔了带去别处连网,服务器还在。HTTP 应用走 Cloudflare Tunnel:cloudflared 从手机发起一条出站连接,Cloudflare 把每个主机名送过来,Caddy 路由到正确的回环服务。没有任何入站路由规则;换网络后隧道会重连,主机名跟着走。Tailscale 对管理面做同样的事,手机电池能撑过搬家的过程。

Surf 的远程浏览器后端需要另一条路:它的直连对延迟敏感、自己终止 TLS,而连它的那台老 iPad 会固定(pin)服务器身份。在家时 Cloudflare DDNS 维持一条仅 DNS 的记录指向当前公网地址,路由器转发一个端口给 Surf,局域网内 iPad 直连手机。漫游场景的解法是:普通 Cloudflare Tunnel 会在 Cloudflare 终止 TLS,而这正是 Surf 的 pin 连接不能接受的,所以他把完整的 Surf TLS 流包进一个普通 WebSocket 里——Cloudflare 看到并转发这个 WebSocket,而真正经过认证的 Surf 连接在里面保持端到端加密。这显然增加了延迟(在外面通常多一个网络往返,iPad 连接他初测约 60 毫秒),但它能穿过一条只需要出站连通性的隧道,在一个 2012 年的操作系统上工作,还不用在 iPad 上装 Tailscale。他形容那是整套配置变得「以最好的方式略显荒唐」的时刻:手机插在家里,他人在办公室,用 MacBook 通过 Tailscale SSH 进工作站和手机,同时用那台老 iPad 通过手机托管的 Surf 上网。VPS 没了,感觉很解放。

实际在跑什么:机器分两层。Android/Termux 层负责硬件、网络、入口和监管(runit、Tailscale、Caddy、Cloudflared、DDNS、运维面板);rooted Linux 常驻层负责应用兼容性和实际负载(Surf 和 Chrome、财务、屏幕共享等)。最吃力的是 Surf,它把现代 Chromium 带给老 iPhone 和 iPad——手机在 Debian 运行时里跑桌面版 Chrome(较新的 arm64 版本)和 Surf 后端,一台 iPad mini 接收 H.264 视频和音频,同时把触摸、键盘、标签页和导航命令发回来。财务追踪器记录周期性收支、一次性交易、月度支出限额,并逐日向前推算余额;与可替换的应用产物不同,它的 SQLite 数据库里是他在乎的状态,所以有自动化的离设备备份和经过测试的恢复路径。最新加的是可观测性:一个原生服务收集八个核心的 CPU 使用率、内存、存储、运行时间、电池、温度、本地与公网可达性以及每一个被发现的 runit 常驻服务,保留有界历史,用内嵌 Vue 界面服务在 https://dash.cmf,只在局域网或 tailnet 内可达。

你该这么干吗?如果你已经有一部还算现代、可 root 的 ARM64 手机闲置,这件事远没有听上去那么荒唐:你得到安静的硬件、低功耗、闪存、Wi-Fi、用于恢复的内建显示屏,以及一块表现得像微型 UPS 的电池。原厂 Android 已经支持所有硬件,Termux 加一个 rooted chroot 足以跑相当多的普通 Linux 软件。但他也划了线:没有自动离设备备份就别放不可替代的数据;别把这些 chroot 当成对抗性负载的隔离;root 扩大了信任边界,Android 仍是一个不寻常的服务器宿主,软件渲染的桌面 Chrome 也打不过带 GPU 的专用工作站。「但它安静、有电池后备、够快、随处可达、可从 Git 复现,而且已经在我家里躺着。我一开始只是想省 VPS 的钱,最后得到了一部跑着我全部个人基础设施的 root 手机,而这不知怎么的更让人满足。」

文末的后记很有意思:这篇文章在 HN 上被讨论了,「以典型的 HN 风格,出乎意料多的对话最后变成了在讨论标题」,他说自己挺享受这场意外的语言学讨论串。

HN 评论精华

这条帖子拿到 542 分、252 条评论。作者的后记没有夸张——讨论的第一大主题真的是标题的语法,第二大主题是电池火灾风险,技术内容反倒排第三。