- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 —— 从想退掉 VPS 开始的实验
- 第一次失败 —— 想丢掉驱动去换 Linux
- 把 Termux 当作宿主控制平面
- 与 Android 电源管理搏斗的清单
- PRoot 与 chroot —— 代价落在系统调用上,不在 CPU 上
- 兼容层不是隔离边界
- 入口 —— 用一条出站连接做出公开服务
- 可复现性 —— 靠 Git,不靠 shell 历史
- 参考资料
引言 —— 从想退掉 VPS 开始的实验
2026 年 8 月 4 日,seg6.space 上发出了一篇叫 my server is a phone now 的文章。记录的是把原本跑在 Hetzner VPS 上的个人服务,搬到抽屉里那台 CMF Phone 1 上的过程。
手机的配置是 8 个 ARM 核心、8GB 内存、128GB 闪存、Wi-Fi 6、5G 基带,以及内置电池。最后这一项其实挺重要,因为从服务器的角度看,它就是一台小型不间断电源。
这篇文章值得读,不在于「用手机跑了服务器」这个结果,而在于作者失败了两次,并从失败里提炼出了准确的教训。尤其第二次失败,对任何和 Linux 容器与兼容层打交道的人都适用。
第一次失败 —— 想丢掉驱动去换 Linux
看起来最干净的路子,是刷一个普通的 Linux 发行版。CMF Phone 1 有 postmarketOS 的设备移植,能启动,设备页面上绿色标记也足够多。
作者没那么仔细看的,是标着「损坏」的那些条目:Wi-Fi、蓝牙、硬件加速。结果是一张 postmarketOS 的启动画面加一块黑屏,那一刻,服务器没了,手机也没了。
恢复也不轻松。刷机工具要求 Windows,于是在 QEMU 里装 Windows,跟 USB 直通和联发科驱动纠缠,最后还是在一台真的 Windows 机器上恢复了出厂镜像。中间还有一段处于软砖状态、只有黑屏的时间。
作者提炼的教训很准确。Android 对这块硬件的每一个零件都已经有能用的驱动了。 Wi-Fi、电源管理、电池、GPU、基带,以及各种厂商特有的细节,全都有。为了换一个更熟悉的用户态就把这些整个丢掉,是一笔亏本买卖。
真正需要的不是让手机变成一台普通的 Linux 机器,而是在 Android 继续负责硬件相关工作的同时,把 Linux 应用稳定地跑起来。
把 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 电源管理搏斗的清单
把手机当服务器用时,最难缠的对手不是 CPU,而是电池优化。用作者的话说:Android 的电池管理,对它本职的事情非常擅长,对一台假装自己是服务器的设备非常糟糕。
Ansible 构建所应用的 Android 宿主配置做了这些事。
- 安装持续的 wake lock
- 关闭 light idle 与 deep idle
- 把 Termux、Termux:Boot、Tailscale 排除在后台限制之外
- 关闭子进程限制器
- 阻止 Wi-Fi 挂起
- 把 Tailscale 设为常开 VPN
但作者强调,比单项设置更重要的是恢复链条。
Android boot
-> Tailscale always-on VPN
-> Termux:Boot
-> runit
-> resident services
-> local and public health checks
有了这条链,手机就不必等人发现,自己就能从重启中恢复。自愈能力不是各项调优之和,而是来自启动路径的设计。这一点不限于手机,对任何服务器都成立。
PRoot 与 chroot —— 代价落在系统调用上,不在 CPU 上
这里是全文的核心。
作者的大部分应用本来就以 Linux ARM64 的 OCI 镜像形式分发。用 proot-distro,就能在 Debian 里跑这些镜像,而且不用改动应用。
PRoot 做的事是这样的。它在用户态拦截文件系统和进程相关的操作,让一个普通的 Termux 进程相信自己住在 Debian 的根文件系统里。不需要 root 权限、也不需要特殊内核,这是它很大的优点。
一般的 Web 服务用这种方式跑得挺好。给每个应用一套验证过的根文件系统、一个 loopback 端口、一份 runit 服务定义,Caddy 再按主机名路由到那个端口。
有一个例外:一个叫 Surf 的远程浏览器负载。进程启动、打开库、路径遍历、读取浏览器配置、搬运抓取数据,全都要穿过 PRoot 的用户态转换层。作者那句话很精确:CPU 是有余量的,但 Chrome 没能高效地够到它。
这句话值得反复咂摸。监控面板上看得到空闲的 CPU,内存也有富余,可应用就是慢。因为瓶颈不在资源的量,而在到达资源的路径。
解法不是去改代码。作者把手机 root 了 —— 不是为了取代 Android,而是为了把同一套 Debian 文件系统正经挂载起来、进入真正的 chroot。runit 依然在 Termux 里管理生命周期,配置也没变,只是负载不再走转换层,而是用原生系统调用抵达内核。用作者的说法:改善并不细微。
一般化之后是这样:兼容层的代价与计算量无关,而与系统调用的频率成正比。 在大数组上做循环的计算密集型程序,跑在 PRoot 之上几乎不吃亏。而要打开成千上万个文件、不停拉起新进程的程序,在同一层上就会垮掉。事先知道某个负载属于哪一边,就是这个判断的全部。
兼容层不是隔离边界
讲完性能之后,作者钉下了一句话,这部分值得引用。
关于 PRoot,他写道:这不是容器边界。所有东西依然共享 Android 的内核、网络命名空间和 Termux 的 UID。作为应用兼容层它极其有用,但也就仅此而已。
搬到 chroot 之后他也保持同样的态度。部署流程是这样的:工作机把每个 ARM64 镜像解析到确切的 digest 并抽出文件系统,Ansible 校验之后安装到手机上。一个小小的 root 助手创建私有挂载命名空间、绑定所需路径、进入 chroot,然后降权并执行原镜像的入口点。手机上既不需要 Docker,也不需要编译器。
而且他补了一句:这些依然不是安全边界,而是兼容环境;私有挂载命名空间主要是为了让挂载和清理保持可预测。
这个区分在手机之外同样成立。由于习惯把容器叫作隔离,我们常常把命名空间给的东西和沙箱给的东西混为一谈。挂载命名空间划分的是文件系统视图,它不划分内核。如果你打算把有敌意的负载放在同一个内核上,只靠命名空间去挡,那是一个需要另一套设计的问题。
入口 —— 用一条出站连接做出公开服务
家用宽带没有固定 IP,也不想在路由器上开 SSH 或应用端口。更何况作者还希望自己带着手机出门时,服务器仍然活着。
HTTP 服务用的是 Cloudflare Tunnel。cloudflared 从手机向外建立一条连接,Cloudflare 按主机名把请求推进那条通道,Caddy 再路由到 loopback 上的服务。
Internet -> Cloudflare Tunnel -> Caddy on 127.0.0.1 -> application on 127.0.0.1
路由器上没有任何入站规则。所以把手机换到另一个网络,隧道会重连,主机名原封不动地跟着走。管理访问由 Tailscale 承担同样的角色。
只有一个服务需要另一条路径。Surf 后端对延迟敏感,自己终止 TLS,而连过来的那台旧 iPad 会对服务器身份做 pin。一般的 Cloudflare Tunnel 会在 Cloudflare 侧终止 TLS,这对做了 pin 的连接来说就等于直接失败。
解法是把 Surf 的整条 TLS 流包进一个普通的 WebSocket 里。Cloudflare 看到的只是 WebSocket,转发即可,而真正经过认证的连接在里面保持端到端加密。代价是延迟。在家门外大约会多出一次网络往返,文中写着 iPad 的连接第一次测的时候大概是 60 毫秒。
只要终止 TLS 的代理遇上对身份做 pin 的客户端,就总会碰上这个问题。包一层是规避这种冲突的标准做法,代价是一次往返。
可复现性 —— 靠 Git,不靠 shell 历史
最后是运维视角的教训。
作者不想要「一台用一周之后就忘光的命令拼出来的宠物服务器」。所以宿主的全部状态都用 Ansible 管理。版本、服务定义、路由、电源设置、密钥、健康检查,全都在同一个私有仓库里。
部署流程的每一个环节都挂着校验。
release or OCI image
-> checksum/digest pinned in Git
-> Ansible over SSH
-> versioned files on the phone
-> atomic current symlink
-> runit service
-> local health check
-> public edge check
发布版本按 digest 或 checksum 固定,安装到带版本号的目录里,再放到一个原子的 current 符号链接后面。checksum 或健康检查失败,部署就停下;回滚就是把固定值改回去再应用一次。应用数据与发布版本是分开的。
密钥的处理也值得留意。手机上没有 Git 检出,所以也就没有密钥。Ansible Vault 的值加密后放在基础设施仓库里,vault 口令则通过让 1Password SSH agent 对一个固定的 challenge 签名来派生。私钥留在 1Password 里,手机不需要访问它。部署时 Ansible 只把各服务需要的运行时值渲染到 Termux 的私有存储里。
作者自己加的附注,我也原样搬过来。他说不会在没有自动离机备份的情况下把不可替代的数据放在这上面;说不会把 chroot 当成对敌意负载的隔离;并且承认 root 会扩大信任边界,以及今后的 Android 更新可能带来新问题。一份好的实验记录,条件就是这些附注和结论写得一样大。