把手机变成服务器:我的 VPS 替代方案
我的服务器现在是一部手机
For a while, my personal infrastructure lived on a small Hetzner VPS. It ran a few web apps, a remote browser called Surf, Caddy, and the usual supporting cast. Nothing particularly serious. It worked, I just didn’t like paying for it.
有一段时间,我的个人基础设施运行在一台小型的 Hetzner VPS 上。它运行着几个 Web 应用、一个名为 Surf 的远程浏览器、Caddy 以及常见的配套服务。没什么特别重要的。它工作正常,我只是不喜欢为它付费。
One of the apps I run, Surf, made the compromise difficult to ignore. The cheapest shared machines were fine until Chrome had real work to do, at which point they felt starved. Dedicated CPU machines fix that, but cost enough each month to make a personal browser feel like a questionable financial commitment.
我运行的一个应用 Surf 让这个妥协变得难以忽视。最便宜的共享机器在 Chrome 有实际工作要做之前都还好,但一旦有工作,它们就显得资源匮乏。专用 CPU 的机器解决了这个问题,但每月成本高得让个人浏览器看起来像一笔可疑的财务承诺。
Buying another machine wasn’t an appealing escape hatch either. DRAM prices have gone completely stupid, so putting together a new box with a comfortable amount of memory felt especially ill timed. I looked at used mini PCs and briefly considered turning my desktop into a server whenever I wasn’t using it. Then I remembered the CMF Phone 1 I already own.
再买一台机器也不是一个有吸引力的逃生通道。DRAM 价格已经变得完全离谱,所以组装一台内存充裕的新机器感觉特别不合时宜。我看了看二手的迷你 PC,也短暂考虑过在我不使用台式机时把它变成服务器。然后我想起了我已经拥有的 CMF Phone 1。
Eight ARM cores, 8 GB of RAM, 128 GB of flash, Wi-Fi 6, a 5G modem, and a built in battery backup, that is all attached to an SoC that I feel is overqualified for sitting in a drawer. And I had already paid for it. After dusting it off and playing with it for a while, I decided to turn the phone into the server.
八个 ARM 核心、8 GB 内存、128 GB 闪存、Wi-Fi 6、5G 调制解调器,以及内置电池备份,这些都连接在一个我觉得放在抽屉里大材小用的 SoC 上。而且我已经为它付过钱了。在掸掉灰尘并玩了一会儿之后,我决定把手机变成服务器。
Today it runs Surf and its managed Chrome instance, my personal finance tracker, a screen sharing service, and a handful of smaller web apps. They survive reboots, deploy from Git, and remain reachable when the phone moves between networks, so this is now the machine that actually replaced the VPS.
如今它运行着 Surf 及其托管的 Chrome 实例、我的个人财务追踪器、一个屏幕共享服务,以及一些较小的 Web 应用。它们能在重启后存活,从 Git 部署,并且在手机切换网络时仍然可以访问,所以现在这台机器实际上取代了 VPS。
the first bad idea: replacing android
第一个坏主意:替换 Android
The cleanest version of this idea seemed to be flashing a normal Linux distribution. The CMF Phone 1 has a postmarketOS device port, it boots, and the device page has enough green boxes to make a reckless person optimistic. I ended up being that person.
这个想法最干净的版本似乎是刷入一个普通的 Linux 发行版。CMF Phone 1 有一个 postmarketOS 设备移植版本,它可以启动,而且设备页面上的绿色框足够多,足以让一个鲁莽的人感到乐观。我最终就成了那个人。
What I paid less attention to was everything marked broken: Wi-Fi, Bluetooth, hardware acceleration, and most of the other things that make the phone useful as a small server. I got as far as the postmarketOS splash screen and a black display. At that point I had neither a server nor a phone.
我较少关注的是所有标记为损坏的部分:Wi-Fi、蓝牙、硬件加速,以及大多数其他让手机作为小型服务器有用的功能。我走到了 postmarketOS 启动画面和黑屏的地步。那时我既没有服务器也没有手机。
Recovering stock Nothing OS turned into its own side quest. The flashing utility needed Windows, so I installed Windows in QEMU, fought USB passthrough and MediaTek drivers, watched the flashing tool hang, then eventually moved the process to an actual Windows installation and restored the factory images.
恢复原版 Nothing OS 变成了一个支线任务。刷机工具需要 Windows,所以我在 QEMU 中安装了 Windows,与 USB 直通和 MediaTek 驱动程序作斗争,看着刷机工具卡住,然后最终将过程转移到实际的 Windows 安装上并恢复了出厂镜像。
There was a moment in the middle of this where the phone was soft bircked and only showed a black screen and I genuinely thought I had converted a perfectly good device into a paperweight.
在这过程中有一个时刻,手机变砖了,只显示黑屏,我真的以为我把一台完好的设备变成了镇纸。
It came back, and lesson learned: Android already has working drivers for every piece of this hardware. Wi-Fi, power management, the battery, the GPU, the modem, and every weird vendor detail already work. Throwing all of that away in pursuit of a more conventional userspace was the wrong trade.
它恢复了,我吸取了教训:Android 已经为这个硬件的每个部件提供了可用的驱动程序。Wi-Fi、电源管理、电池、GPU、调制解调器以及每个奇怪的供应商细节都已经能正常工作。为了追求更传统的用户空间而抛弃这一切是错误的取舍。
I didn’t actually need the phone to become a normal Linux machine. I needed it to run Linux applications reliably while Android continued doing the hardware specific work it is good at.
我实际上并不需要手机变成一台普通的 Linux 机器。我需要它可靠地运行 Linux 应用程序,同时让 Android 继续做它擅长的硬件相关工作。
termux is the host operating system
termux 是宿主操作系统
The second attempt kept stock Android and treated Termux as the host environment.
第二次尝试保留了原版 Android,并将 Termux 视为宿主环境。
Termux gives me OpenSSH, runit, Caddy, Cloudflared, package management, and normal enough Unix tooling. Termux:Boot starts the supervisor and SSH after a reboot. Tailscale gives the phone a stable private address, so from any machine on my tailnet I can just run:
Termux 为我提供了 OpenSSH、runit、Caddy、Cloudflared、包管理以及足够正常的 Unix 工具。Termux:Boot 在重启后启动监督程序和 SSH。Tailscale 为手机提供了一个稳定的私有地址,所以从我的 tailnet 上的任何机器上,我都可以直接运行:
ssh cmfssh cmfTermux is not a virtual machine. Its processes still execute against Android’s Linux kernel, but its Bionic based userspace is different enough from an ordinary Debian installation that existing Linux application images cannot simply be dropped into it. That split ended up being useful, though: Termux could remain the small host control plane while each application brought the Linux filesystem it expected.
Termux 不是虚拟机。它的进程仍然在 Android 的 Linux 内核上执行,但其基于 Bionic 的用户空间与普通的 Debian 安装有足够大的差异,以至于现有的 Linux 应用程序镜像不能简单地放入其中。不过,这种分离最终被证明是有用的:Termux 可以保持为小型宿主控制平面,而每个应用程序则带来它期望的 Linux 文件系统。
The actual services are supervised by runit. Android’s battery management is very good at its normal job and very bad for a device pretending to be a server, so I had my Ansible build also apply an Android host profile: it installs a persistent wake lock, disables light and deep idle, exempts Termux, Termux:Boot, and Tailscale from background restrictions, disables the child process limiter, prevents Wi-Fi suspension, and configures Tailscale as the always on VPN.
实际的服务由 runit 监督。Android 的电池管理在正常工作中非常出色,但对于一台假装是服务器的设备来说却非常糟糕,所以我的 Ansible 构建还应用了一个 Android 主机配置文件:它安装了持久的唤醒锁,禁用了浅层和深层空闲,将 Termux、Termux:Boot 和 Tailscale 从后台限制中豁免,禁用了子进程限制器,防止 Wi-Fi 挂起,并将 Tailscale 配置为始终开启的 VPN。
The recovery chain matters more than any individual setting. Android boots, always on VPN brings Tailscale back, Termux:Boot starts runit, runit starts every resident service, and health checks verify the local and public paths. The phone can reboot without waiting for me to notice.
恢复链比任何单个设置都重要。Android 启动,始终开启的 VPN 带回 Tailscale,Termux:Boot 启动 runit,runit 启动每个常驻服务,健康检查验证本地和公共路径。手机可以在不需要我注意的情况下重启。
Android boot
-> Tailscale always-on VPN
-> Termux:Boot
-> runit
-> resident services
-> local and public health checksAndroid boot
-> Tailscale always-on VPN
-> Termux:Boot
-> runit
-> resident services
-> local and public health checksThis isn’t a conventional Linux server. There is no systemd, no normal Docker daemon, and no reason to pretend otherwise. But it is a Linux kernel with a very capable userland sitting on top of it, and that turns out to be enough.
这不是一台传统的 Linux 服务器。没有 systemd,没有正常的 Docker 守护进程,也没有必要假装有。但它是一个 Linux 内核,上面有一个非常强大的用户态,而这被证明已经足够了。
the second bad idea: proot
第二个坏主意:proot
Most of my applications already shipped as Linux ARM64 OCI images. proot-distro made those surprisingly easy to run under Debian without changing the applications themselves.
我的大多数应用已经以 Linux ARM64 OCI 镜像的形式发布。proot-distro 让这些应用在 Debian 下运行变得出奇地简单,无需修改应用本身。
PRoot intercepts filesystem and process operations in userspace and makes a regular Termux process believe it lives inside a Debian root filesystem. It isn’t a container boundary. Everything still shares Android’s kernel, network namespace, and Termux UID. But as an application compatibility layer, it is extremely useful because it needs neither root nor a special kernel.
PRoot 在用户空间拦截文件系统和进程操作,让一个普通的 Termux 进程相信自己运行在 Debian 根文件系统内。它不是一个容器边界。所有内容仍然共享 Android 的内核、网络命名空间和 Termux UID。但作为一个应用兼容层,它非常有用,因为它既不需要 root,也不需要特殊内核。
The ordinary web services initially ran fine this way. Each of my applications got a verified root filesystem, a loopback port, and a runit service. Caddy ran directly in Termux and routed hostnames to those ports.
普通的 Web 服务最初以这种方式运行良好。我的每个应用都获得了经过验证的根文件系统、一个回环端口和一个 runit 服务。Caddy 直接在 Termux 中运行,并将主机名路由到这些端口。
The performance/latency sensitive Surf browser workload was the exception. Starting processes, opening libraries, walking paths, reading browser profiles, and shuffling capture data all crossed PRoot’s userspace translation layer. There was CPU available, but Chrome could not reach it efficiently. So I rooted the phone, not to replace Android, but to mount the same Debian filesystem properly and enter it with a real chroot.
对性能/延迟敏感的 Surf 浏览器工作负载是个例外。启动进程、打开库、遍历路径、读取浏览器配置文件以及整理捕获数据,所有这些都经过 PRoot 的用户空间转换层。虽然有可用的 CPU,但 Chrome 无法高效地使用它。因此,我 root 了手机,不是为了替换 Android,而是为了正确挂载相同的 Debian 文件系统,并通过真正的 chroot 进入它。
Runit still owned the lifecycle from Termux, configuration still came from the same place, and application data still lived in Termux storage. The workload simply reached Android’s kernel through native syscalls instead of PRoot. The improvement was not subtle!
Runit 仍然从 Termux 管理生命周期,配置仍然来自相同的位置,应用数据仍然存储在 Termux 存储中。工作负载只是通过原生系统调用访问 Android 内核,而不是通过 PRoot。改进是显著的!
Once that path was solid, leaving the smaller residents under PRoot stopped making much sense. They now run the same way. My workstation resolves each ARM64 image to an exact digest and exports its filesystem; Ansible verifies and installs it on the phone. A small root helper creates a private mount namespace, binds the required paths, enters the filesystem with chroot, drops privileges, and starts the original image entrypoint.
一旦这条路径稳固,让较小的常驻程序留在 PRoot 下就没什么意义了。它们现在以相同的方式运行。我的工作站将每个 ARM64 镜像解析为精确的摘要并导出其文件系统;Ansible 在手机上验证并安装它。一个小型 root 辅助程序创建私有挂载命名空间,绑定所需路径,使用 chroot 进入文件系统,降低权限,并启动原始镜像的入口点。
Neither Docker nor a compiler needs to exist on the phone. These are still compatibility environments rather than security boundaries: the residents share Android’s kernel and network stack, while the private mount namespaces mainly keep mounts and cleanup predictable.
手机上既不需要 Docker,也不需要编译器。这些仍然是兼容环境,而不是安全边界:常驻程序共享 Android 的内核和网络栈,而私有挂载命名空间主要使挂载和清理变得可预测。
I also spent far too long trying to bridge Debian’s graphics stack to the phone’s Mali GPU through VirGL and Android Vulkan. I got hardware compositing checkmarks alongside corrupt pages and worse performance… The boring software rendered path turned out to perform better.
我还花了太长时间尝试通过 VirGL 和 Android Vulkan 将 Debian 的图形栈桥接到手机的 Mali GPU。我得到了硬件合成对勾,但页面损坏,性能更差……无聊的软件渲染路径结果反而更好。
infrastructure! not a pile of shell history
基础设施!不是一堆 shell 历史记录
By this point the phone could run everything, but I didn’t want a pet server assembled from commands I would forget in a week. I moved the entire host into an Ansible managed state: versions, service definitions, routes, power settings, secrets, and health checks all live in one private repository.
到这个时候,手机已经能运行一切了,但我不想要一个由我一周后就会忘记的命令拼凑起来的宠物服务器。我把整个主机迁移到了Ansible管理状态:版本、服务定义、路由、电源设置、密钥和健康检查都存放在一个私有仓库中。
The deployment flow is roughly:
部署流程大致如下:
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力