我的服务器现在是一部手机
摘要
作者用一台运行 Termux 的 CMF Phone 1 家用服务器取代了 Hetzner VPS,详细介绍了安装 postmarketOS 的种种陷阱,最终使用 Android 搭配 Termux、runit、Tailscale 和 Caddy 来托管 Surf 和财务追踪器等 Web 应用。
暂无内容
查看缓存全文
缓存时间: 2026/08/09 02:22
# 我的服务器现在是一台手机
来源:http://seg6.space/posts/phone-server/
有一段时间,我的个人基础设施跑在一台小型的 Hetzner VPS 上。它运行着几个 Web 应用、一个名为 Surf 的远程浏览器(http://seg6.space/posts/resurrecting-ipad-mini/)、Caddy,以及一些常见的辅助服务。没什么特别重要的东西。它一直能用,我只是不喜欢为此付费。
我运行的其中一个应用,Surf,让这个妥协变得难以忽视。最便宜的共享机器在 Chrome 有真正的活要干之前还算可以,一旦工作负载上来,就会觉得资源匮乏。专用 CPU 的机器能解决这个问题,但每月的费用高到让一个个人浏览器看起来像一笔可疑的财务承诺。
买另一台机器也不是什么有吸引力的逃逸方案。DRAM 价格已经彻底疯了(https://www.trendforce.com/presscenter/news/20260331-12995.html),所以组装一台内存充足的新机器显得尤其不合时宜。我看了看二手迷你主机,也短暂考虑过在不用电脑时把桌面机当成服务器。然后我想起了那台我已经拥有的 CMF Phone 1。
八个 ARM 核心、8 GB 内存、128 GB 闪存、Wi-Fi 6、一个 5G 调制解调器,还有内置电池作为备用电源,这些都集成在一颗我觉得放在抽屉里实在大材小用的 SoC 上。而且我早就为它付过钱了。把它擦干净把玩了一阵子之后,我决定把手机变成服务器。
如今它运行着 Surf 及其管理的 Chrome 实例、我的个人财务跟踪器、一个屏幕共享服务,以及几个较小的 Web 应用。它们能在重启后存活,通过 Git 部署,并且在手机切换网络时依然可访问,所以这台机器现在实际上取代了 VPS。
## 第一个坏主意:替换 Android
这个想法最“干净”的版本似乎是刷入一个普通的 Linux 发行版。CMF Phone 1 有一个 postmarketOS 的设备移植版本,它能启动,而且设备页面上的绿色框足够多,足以让一个鲁莽的人感到乐观。而我最终就成了那个人。
我没有怎么关注的是那些标记为损坏的东西:Wi-Fi、蓝牙、硬件加速,以及大多数让手机作为小型服务器有用的功能。我最多走到了 postmarketOS 的启动画面,然后就是黑屏。那时我既没有服务器,也没有手机。
恢复原厂的 Nothing OS 本身变成了一次支线任务。刷机工具需要 Windows,所以我在 QEMU 里装了 Windows,与 USB 直通和 MediaTek 驱动搏斗,看着刷机工具卡住,最后把流程挪到一个真正的 Windows 安装上,恢复了出厂镜像。
中间有一刻,手机处于软砖状态,只显示黑屏,我真的以为自己把一台完好的设备变成了一块镇纸。
它最后还是回来了,教训也学到了:Android 已经为这块硬件的每一个部分提供了可用的驱动程序。Wi-Fi、电源管理、电池、GPU、调制解调器,以及各种奇怪的厂商细节,全都已经能正常工作。为了追求一个更传统的用户空间而把这一切丢掉,是错误的取舍。
我实际上并不需要手机变成一台普通的 Linux 机器。我需要它可靠地运行 Linux 应用程序,同时让 Android 继续做它擅长的硬件相关工作。
## Termux 就是宿主操作系统
第二次尝试保留了原版 Android,并把 Termux 当作宿主环境。
Termux 给了我 OpenSSH、runit、Caddy、Cloudflared、包管理,以及足够正常的 Unix 工具链。Termux:Boot 在重启后启动监督进程和 SSH。Tailscale 给手机一个稳定的私有地址,所以从我 tailnet 上的任何机器,我都可以直接运行:
```
ssh cmf
```
Termux 不是虚拟机。它的进程仍然是针对 Android 的 Linux 内核执行的,但它基于 Bionic 的用户空间与普通的 Debian 安装差异很大,以至于现有的 Linux 应用镜像不能直接丢进去。不过,这种割裂最终被证明是有用的:Termux 可以作为小巧的主机控制平面,而每个应用则带上它自己期望的 Linux 文件系统。
实际的服务由 runit 监督。Android 的电池管理在它正常的本职工作方面非常出色,但对于一台假装自己是服务器的设备来说就非常糟糕了,所以我让 Ansible 构建同时应用了一个 Android 主机配置文件:它安装一个持久的唤醒锁,禁用轻度和深度空闲,将 Termux、Termux:Boot 和 Tailscale 排除在后台限制之外,禁用子进程限制器,防止 Wi-Fi 挂起,并将 Tailscale 配置为始终开启的 VPN。
恢复链路比任何单个设置都重要。Android 启动,始终开启的 VPN 把 Tailscale 带回来,Termux:Boot 启动 runit,runit 启动每个常驻服务,健康检查验证本地和公网路径。手机可以在无需我注意的情况下自行重启。
```
Android 启动
-> Tailscale 始终开启 VPN
-> 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。改进可不是微小的!
一旦这条路走通了,让那些较小的常驻服务继续留在 PRoot 下就没什么意义了。它们现在也以同样的方式运行。我的工作站把每个 ARM64 镜像解析到精确的摘要(digest)并导出其文件系统;Ansible 在手机上验证并安装它。一个小型的 root 辅助程序创建私有挂载命名空间,绑定所需的路径,用 `chroot` 进入文件系统,降低权限,然后启动原始镜像的入口点。
手机上既不需要 Docker,也不需要编译器。这些仍然是兼容环境,而不是安全边界:常驻服务共享 Android 的内核和网络栈,而私有挂载命名空间主要是让挂载和清理变得可预测。
我还花了太长时间尝试通过 VirGL 和 Android Vulkan 把 Debian 的图形栈桥接到手机的 Mali GPU 上。我得到了硬件合成的小勾,同时伴随着损坏的页面和更差的性能……而那个无聊的软件渲染路径结果反而性能更好。
## 基础设施!而不是一堆 shell 历史
到这个时候,手机已经能运行一切了,但我不想拥有一台靠一周后就会忘记的命令拼凑起来的宠物服务器。我把整个主机迁移到 Ansible 管理的状态中:版本、服务定义、路由、电源设置、密钥和健康检查,全部放在一个私有仓库里。
部署流程大致是:
```
发布或 OCI 镜像
-> 校验和/摘要固定在 Git 中
-> 通过 SSH 使用 Ansible
-> 手机上的版本化文件
-> 原子 current 符号链接
-> runit 服务
-> 本地健康检查
-> 公网边缘检查
```
发布版本通过摘要或校验和固定,安装到版本化目录中,背后是一个原子的 `current` 符号链接。校验和或健康检查失败会停止部署,回滚意味着还原固定版本并重新应用。应用数据与发布版本分开存放。
在小规模手动引导之后:安装三个 Android 应用、给手机 root、授予 Termux 超级用户访问权限、授权 SSH,剩下的就都由同一个仓库接管。从我的工作站,让主机达到声明状态是刻意保持简单的:
```
make phone
make phone-status
make phone-edge-check
```
再次应用不会替换未更改的运行时文件,也不会重启健康的常驻服务。更重要的是,如果这台手机死了,这份配置仍然有用:另一台可 root 的 ARM64 手机可以被带到相同的状态,而无需重建一份 shell 历史。
密钥不会存储在手机的 Git 检出中,因为根本没有 Git 检出。Ansible Vault 的值以加密形式存放在基础设施仓库中。Vault 密码是通过让我的 1Password SSH 代理签署一个固定挑战来推导的,所以私钥留在 1Password 里,手机永远不需要访问它。在部署期间,Ansible 只把每个服务需要的运行时值渲染到 Termux 的私有存储中。
## 把流量送到位于家庭网络后面的手机
下一个要解决的是入口问题。我的家庭连接没有 VPS 那种静态服务器配置,而且我也不想把 SSH 或一堆随机的应用端口通过路由器暴露出去。我还希望手机在一个重要意义上仍然是手机:我应该能拔掉它,带到别处,连上互联网,然后我的服务器仍然可用。
HTTP 应用使用 Cloudflare Tunnel。Cloudflared 从手机发出一个出站连接,Cloudflare 通过它发送每个主机名,Caddy 再把请求路由到正确的回环服务。
```
互联网
-> Cloudflare Tunnel
-> Caddy 在 127.0.0.1
-> 应用在 127.0.0.1
```
这些服务没有入站路由器规则。Cloudflared 只需要出站连接,所以如果我把手机移到另一个网络,隧道会重新连接,主机名也会跟着它走。Tailscale 对管理访问也是如此。手机的电池可以撑过移动过程,而公网服务并不关心底下是哪个 Wi-Fi。
不过,Surf 远程浏览器后端需要一条不同的路径。它的直接连接对延迟敏感,自己终止 TLS,而连接它的旧 iPad 固定了服务器身份。在家里,Cloudflare DDNS 保持一条仅 DNS 的记录指向当前公网地址,路由器把端口转给 Surf。在 LAN 上,iPad 直接连接到手机。
这仍然留下了漫游的问题。普通的 Cloudflare Tunnel 在 Cloudflare 终止 TLS,而这恰恰是 Surf 固定连接不想要的。解决方案是将完整的 Surf TLS 流包装在一个普通的 WebSocket 内部。Cloudflare 看到并转发 WebSocket,但真正经过认证的 Surf 连接在里面保持了端到端的加密。
这显然会增加延迟,在家庭以外我通常看到大约多一个网络往返,而我第一次测试时 iPad 连接大约是 60 毫秒。但它在只需要出站连接的隧道中工作,操作系统还是 2012 年的,而且不需要在 iPad 上安装 Tailscale。
那台手机设置就在那一刻变得有点离谱,而且是以最好的方式。我把它留在家里插着电,去办公室,从 MacBook 通过 Tailscale SSH 到我的工作站和手机,然后用那台原始的 iPad,通过手机托管的 Surf 实例进行浏览。VPS 没了,这感觉令人解放。
## 实际在跑什么
这台机器有两层。Android 和 Termux 拥有硬件、网络、入口和监督。Linux 常驻服务获得它们期望的文件系统,其余部分则不去妨碍宿主机。
| 层 | 职责 | 组件 |
|---|---|---|
| Android / Termux 宿主 | 硬件、网络、入口、监督 | runit、Tailscale、Caddy、Cloudflared、DDNS、运维仪表盘 |
| root 后的 Linux 常驻服务 | 应用兼容性和工作负载 | Surf 和 Chrome、Finances、Screen Share、一些其他应用 |
要求最高的工作负载是 Surf,它把现代 Chromium 浏览器带到旧款 iPhone 和 iPad 上。手机在其 Debian 运行时中运行桌面版 Chrome(最近的 arm64 版 Chrome)和 Surf 后端,一台 iPad mini 接收 H.264 视频和音频,同时把触摸、键盘、标签页和导航命令发送回去。Surf 正是我最初如此在意系统调用开销和延迟的原因。
在它旁边,手机托管着我的个人财务跟踪器。它记录经常性收入和支出、单次交易、每月消费限额,并逐日预测我的余额。与可替代的应用工件不同,它的 SQLite 数据库包含我在意的状态,所以它有自动化的异地备份和经过测试的恢复路径。
这些名字都不是被塞进某个宏大的“手机服务器框架”里的。一个新的常驻服务只是一个不可变的工件、一个 runit 定义、一个明确的数据契约、一个健康检查,以及一个可选的 Caddy 路由。
最新的新增功能是可观测性。我仍然可以 SSH 进去、检查 runit、跟踪日志、检查公网路由,并直接查询 Android,但我不再需要仅仅为了看一眼机器当前状况而做这些事:一个原生服务会收集全部八个核心的 CPU 使用率、内存、存储、运行时间、电池、温度、本地和公网可达性,以及每一个被发现的 runit 常驻服务。它保存有界的历史记录,并在 `https://dash.cmf` 提供一个内嵌的 Vue 界面,只能通过 LAN 或 tailnet 访问。
仪表盘
日志视图在运行时发现相同的服务目录,所以添加一个新的常驻服务不需要让 UI 知道它的名字。这比 SSH 进去只为想起是哪个东西在吵闹要好得多。
## 你应该这么做吗?
如果你已经有一台还算现代、可 root 的 ARM64 手机闲置不用,这比听起来要靠谱得多。你会得到安静的硬件、低功耗、闪存、Wi-Fi、用于恢复的内置显示屏,以及一块像小型 UPS 一样的电池。原生 Android 已经支持硬件,而 Termux 加上 root 后的 chroot 足以运行数量惊人的普通 Linux 软件。
我不会把不可替代的数据放在没有自动化异地备份的设备上,也不会把 chroot 当作抵御恶意工作负载的隔离。Root 会扩大信任边界,Android 仍然是一个不寻常的服务器宿主,软件渲染的桌面 Chrome 也不可能胜过带 GPU 的专用工作站。
但对于少数几个个人服务来说,尤其是当替代方案是无限期地为一台要么慢要么贵得烦人的 VPS 付费时,这是一个真正有用的选项,而不仅仅是噱头。
这台手机仍然是一台奇怪的服务器。它共享一个内核,Android 偶尔需要被提醒不要“优化”它,而未来的 Android 更新也总可能带来新的惊喜。
但它安静、有电池后备、速度足够、可从任何地方访问、可以从 Git 复现,而且已经放在我家里了。我开始做这件事是为了在 VPS 上省钱。结果我得到了一台运行着我的个人基础设施的 root 手机,而不知怎的,这感觉更令人满足。:)
相似文章
@wongmjane:我将使用了8年的手机复活成了一台Hermes Agent服务器——用postmarketOS(基于ARM64的Alpine Linux)替换了Android系统……
用户将一台用了8年的手机改造成Hermes Agent服务器,运行在ARM64的postmarketOS上,通过Matrix进行端到端加密聊天,减少了电子垃圾,也无需再购买Mac mini。
落厕碎屏手机网页服务器
作者复活了一部被摔碎、进水的三星A70手机,利用USB-C集线器提供电源和以太网连接,将其改造为一个功能性的网页服务器,展示了旧手机如何被重新利用。
Show HN:将现代Linux移植到一款十年前的QWERTY手机
一位开发者将Linux移植到一款十年前的QWERTY手机上,项目历史与进展记录在git仓库中。
我正在把我的手机从Android切换到Linux
一位用户详细描述了他们从Android切换到Fairphone 4上的SailfishOS的过程,比较了移动Linux的选项,并概述了当前的权衡和挑战。
我把一部安卓手机改造成了支持Vulkan加速的本地大模型节点(GGUF + LiteLLM + Tailscale)
一部安卓手机被重新用作便携式GGUF推理服务器,支持Vulkan加速,并通过LiteLLM暴露兼容OpenAI的端点,借助Tailscale网格集成到自托管的AI集群中。