My server is a phone now

My server is a phone now

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

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. 购买另一台机器也不是一个理想的方案。内存价格涨得离谱,所以组装一台内存充足的新机器显得时机非常不对。我查看了二手迷你 PC,并短暂考虑过在不使用时将我的台式机变成服务器。

Then I remembered the CMF Phone 1 I already own. 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. 然后我想起了我已有的 CMF Phone 1。八核 ARM 处理器、8GB 内存、128GB 闪存、Wi-Fi 6、5G 调制解调器,以及内置的电池备份,所有这些都集成在一个 SoC 上,我觉得让它躺在抽屉里简直是大材小用。而且我已经为它付过钱了。

After dusting it off and playing with it for a while, I decided to turn the phone into the server. 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 直通和联发科驱动程序作斗争,看着刷机工具卡死,最后不得不把整个过程转移到一台真正的 Windows 电脑上,才成功恢复了出厂镜像。

There was a moment in the middle of this where the phone was soft bricked 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. 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: ssh cmf 第二次尝试保留了原厂 Android,并将 Termux 作为宿主环境。Termux 为我提供了 OpenSSH、runit、Caddy、Cloudflared、包管理以及足够标准的 Unix 工具。Termux:Boot 在重启后启动守护进程和 SSH。Tailscale 为手机提供了一个稳定的私有地址,因此在我的 Tailnet 网络中的任何机器上,我只需运行:ssh cmf

Termux 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. The actual services are supervised by runit. Termux 不是虚拟机。它的进程仍然在 Android 的 Linux 内核上执行,但其基于 Bionic 的用户空间与普通的 Debian 安装有很大不同,因此现有的 Linux 应用镜像不能直接放入其中运行。不过,这种差异最终证明是有用的:Termux 可以作为轻量级的宿主控制平面,而每个应用都自带其所需的 Linux 文件系统。实际的服务由 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. Android 的电池管理在正常使用时表现出色,但对于一台伪装成服务器的设备来说却非常糟糕,所以我让我的 Ansible 构建脚本应用了一个 Android 宿主配置:它安装了一个持久的唤醒锁(wake lock),禁用了轻度和深度休眠,将 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 checks Android 启动 -> Tailscale 始终开启的 VPN -> Termux:Boot -> runit -> 驻留服务 -> 本地和公共健康检查

This 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. PRoot intercepts filesystem and process operations in userspace and makes a regular Termux process believe it lives inside a Debian root filesystem. 我的大多数应用都已经以 Linux ARM64 OCI 镜像的形式发布。proot-distro 让这些应用在 Debian 下运行变得出奇地简单,而无需更改应用本身。PRoot 在用户空间拦截文件系统和进程操作,让普通的 Termux 进程认为自己运行在 Debian 根文件系统中。

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. 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. 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. 它不是容器边界。一切仍然共享 Android 的内核、网络命名空间和 Termux UID。但作为一个应用兼容层,它非常有用,因为它既不需要 root 权限,也不需要特殊的内核。普通的 Web 服务最初以这种方式运行良好。我的每个应用都获得了一个经过验证的根文件系统、一个回环端口和一个 runit 服务。Caddy 直接在 Termux 中运行,并将主机名路由到这些端口。对性能/延迟敏感的 Surf 浏览器工作负载是个例外。启动进程、打开库、遍历路径、读取浏览器配置文件以及处理捕获数据,所有这些操作都需要经过 PRoot 的用户空间转换。