📖 Перевод статьи с seg6.space · 4 августа 2026
август 4, 2026 · автор: seg6

Мой сервер теперь — телефон

Получение root-прав на CMF Phone 1 для запуска личной инфраструктуры дома.

Какое-то время моя личная инфраструктура жила на небольшом VPS у Hetzner. Там крутилось несколько веб-приложений, удалённый браузер под названием Surf, Caddy и привычный набор вспомогательных сервисов. Ничего особенно серьёзного. Всё работало — просто мне не нравилось за это платить.

Одно из приложений — Surf — сделало этот компромисс труднопереносимым. Самые дешёвые shared-машины были нормальными, пока Chrome не начинал делать реальную работу — в этот момент им не хватало ресурсов. Выделенные процессоры (dedicated CPU) это исправляют, но стоят достаточно дорого каждый месяц, чтобы личный браузер стал финансово сомнительным вложением.

Покупка ещё одной машины тоже не казалась заманчивым выходом. Цены на DRAM окончательно сошли с ума, так что собирать новый бокс с комфортным объёмом памяти было особенно не вовремя. Я посматривал на б/у мини-ПК и даже на мгновение подумывал превратить свой десктоп в сервер, когда им не пользуюсь. А потом вспомнил, что у меня уже есть CMF Phone 1.

Восемь ARM-ядер, 8 ГБ ОЗУ, 128 ГБ флэш-памяти, Wi-Fi 6, 5G-модем и встроенная батарея как ИБП — и всё это на SoC, которая, по моему мнению, слишком хороша, чтобы просто лежать в ящике. А я уже за неё заплатил. Смахнув пыль и немного поигравшись, я решил превратить телефон в сервер.

Сегодня он крутит Surf с управляемым экземпляром Chrome, мой личный финансовый трекер, сервис демонстрации экрана и горсть мелких веб-приложений. Они переживают перезагрузки, разворачиваются из Git и остаются доступными, когда телефон переключается между сетями. Так что именно эта машина теперь по-настоящему заменила VPS.

01Первая плохая идея: заменить Android

Самый чистый вариант казался таким: прошить обычный Linux-дистрибутив. У CMF Phone 1 есть порт postmarketOS, он загружается, а на странице устройства достаточно зелёных галочек, чтобы безрассудный человек поверил в успех. Этим безрассудным человеком оказался я.

Чему я уделил меньше внимания — так всему, что отмечено как сломанное: Wi-Fi, Bluetooth, аппаратное ускорение и большинство других вещей, которые делают телефон полезным как небольшой сервер. Я добрался только до заставки postmarketOS и чёрного экрана. В этот момент у меня не было ни сервера, ни телефона.

Восстановление стокового Nothing OS превратилось в отдельный квест. Утилита прошивки требовала Windows, поэтому я установил Windows в QEMU, бился с USB-passthrough и драйверами MediaTek, смотрел, как прошивальщик зависает, а потом перенёс процесс на настоящую установку Windows и восстановил заводские образы.

⚠ Момент паники
В какой-то момент телефон был софт-брикнут — только чёрный экран. Я всерьёз думал, что превратил вполне рабочий аппарат в пресс-папье.

Он ожил, и урок был усвоен: Android уже имеет рабочие драйверы для каждой части этого железа. Wi-Fi, управление питанием, батарея, GPU, модем и все странные вендорские детали уже работают. Выбросить всё это ради более привычного userspace — ошибочный компромисс.

На самом деле мне не нужно было, чтобы телефон стал обычной Linux-машиной. Мне нужно было, чтобы он надёжно запускал Linux-приложения, пока Android продолжал делать то, в чём он хорош — работу с железом.

02Termux — это хост-операционка

Вторая попытка оставила стоковый Android и сделала Termux основной средой.

Termux даёт мне OpenSSH, runit, Caddy, Cloudflared, пакетный менеджер и достаточно обычный Unix-инструментарий. Termux:Boot запускает супервизор и SSH после перезагрузки. Tailscale даёт телефону стабильный приватный адрес, так что с любой машины в моём tailnet я просто запускаю:

shell
ssh cmf

Termux — не виртуальная машина. Его процессы выполняются поверх ядра Linux Android, но его Bionic-based userspace достаточно отличается от обычной установки Debian, так что существующие образы Linux-приложений нельзя просто так туда скопировать. Это разделение, впрочем, оказалось полезным: Termux остался небольшим control plane хоста, а каждое приложение приносило ту файловую систему Linux, которую оно ожидало.

Сервисы управляются через runit. Управление батареей Android отлично справляется со своей обычной работой и очень плохо — с устройством, притворяющимся сервером. Поэтому мой Ansible-билд также применяет Android-профиль хоста: устанавливает постоянный wake lock, отключает light и deep idle, исключает Termux, Termux:Boot и Tailscale из фоновых ограничений, отключает лимитер дочерних процессов, предотвращает засыпание Wi-Fi и настраивает Tailscale как always-on VPN.

🔑 Ключевой момент
Цепочка восстановления важнее любой отдельной настройки. Android загружается → always-on VPN поднимает Tailscale → Termux:Boot запускает runit → runit запускает все резидентные сервисы → health-check'и проверяют локальный и публичный пути. Телефон может перезагружаться без моего участия.
boot chain
Android boot
  → Tailscale always-on VPN
  → Termux:Boot
  → runit
  → resident services
  → local and public health checks

Это не обычный Linux-сервер. Здесь нет systemd, нет обычного Docker-демона, и нет смысла притворяться. Но это ядро Linux с очень способным userland поверх него — и этого, как оказалось, достаточно.

03Вторая плохая идея: PRoot

Большая часть моих приложений уже поставлялась как Linux ARM64 OCI-образы. proot-distro делал их удивительно простыми в запуске под Debian без изменения самих приложений.

PRoot перехватывает файловые и процессные операции в userspace и заставляет обычный процесс Termux верить, что он живёт внутри Debian root filesystem. Это не граница контейнера. Всё по-прежнему использует ядро Android, сетевое пространство имён и UID Termux. Но как слой совместимости приложений он крайне полезен, потому что не требует ни root, ни специального ядра.

Обычные веб-сервисы изначально работали таким образом. Каждое приложение получило проверенный root filesystem, loopback-порт и runit-сервис. Caddy работал напрямую в Termux и маршрутизировал имена хостов на эти порты.

Исключением стала чувствительная к производительности и задержкам нагрузка — браузер Surf. Запуск процессов, открытие библиотек, обход путей, чтение профилей Chrome и пересылка данных захвата — всё проходило через слой трансляции PRoot в userspace. Процессор был доступен, но Chrome не мог добраться до него эффективно. Тогда я получил root на телефоне — не чтобы заменить Android, а чтобы смонтировать ту же Debian-файловую систему должным образом и войти в неё через настоящий chroot.

⚡ Результат
Runit по-прежнему управлял жизненным циклом из Termux, конфигурация шла оттуда же, данные приложений жили в хранилище Termux. Нагрузка просто достигала ядра Android через нативные сисколы вместо PRoot. Улучшение было наглядным.

Когда этот путь стал надёжным, оставлять мелких резидентов под PRoot потеряло смысл. Теперь они работают так же. Моя рабочая станция резолвит каждый ARM64-образ до точного дайджеста и экспортирует его файловую систему; Ansible проверяет и устанавливает её на телефон. Небольшой root-хелпер создаёт приватное mount-пространство, биндит нужные пути, входит в файловую систему через chroot, сбрасывает привилегии и запускает оригинальную entrypoint образа.

Ни Docker, ни компилятор не нужны на телефоне. Это по-прежнему среды совместимости, а не границы безопасности: резиденты делят ядро и сетевой стек Android, а приватные mount-namespace нужны в основном для предсказуемости монтирования и очистки.

Также я потратил слишком много времени, пытаясь соединить графический стек Debian с Mali GPU телефона через VirGL и Android Vulkan. Я получил галочки аппаратного композитинга рядом с битыми страницами и худшей производительностью… Скучный софтверный рендеринг оказался быстрее.

04Инфраструктура! А не кучка из истории шелла

К этому моменту телефон мог запустить всё, но мне не нужен был «pet-сервер», собранный из команд, которые я забуду через неделю. Я перевёл весь хост в управляемое Ansible-состояние: версии, определения сервисов, маршруты, настройки питания, секреты и health-check'и — всё живёт в одном приватном репозитории.

Поток развёртывания примерно такой:

deploy flow
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

Релизы фиксируются по дайджесту или контрольной сумме и устанавливаются в версионированные директории за атомарным симлинком current. Проваленная контрольная сумма или health-check останавливает развёртывание; откат означает возврат пина и повторное применение. Данные приложений живут отдельно от релизов.

После небольшого ручного бутстрапа — установить три Android-приложения, получить root, дать Termux доступ суперпользователя и авторизовать SSH — тот же репозиторий владеет всем остальным. С рабочей станции привести хост к заданному состоянию намеренно просто:

makefile
make phone
make phone-status
make phone-edge-check

Повторное применение не заменяет неизменённые runtime-файлы и не перезапускает здоровых резидентов. Что важнее — конфигурация полезна, если телефон умрёт: другой рутируемый ARM64-телефон можно привести к тому же состоянию без реконструкции истории шелла.

🔐 Секреты
Секреты не хранятся в Git-checkout на телефоне, потому что никакого Git-checkout на нём нет. Значения Ansible Vault лежат зашифрованными в инфраструктурном репозитории. Пароль от vault'а вычисляется через запрос к моему 1Password SSH-агенту подписать фиксированный challenge — приватный ключ остаётся в 1Password, и телефону никогда не нужен к нему доступ. Во время развёртывания Ansible рендерит только runtime-значения, нужные каждому сервису, в приватное хранилище Termux.

05Как пустить трафик на телефон за домашним интернетом

Следующей проблемой был ingress. Мой домашний интернет не идёт со статикой, как у VPS, и я не хочу открывать SSH или кучу случайных портов приложений через роутер. Также я хотел, чтобы телефон оставался телефоном в одном важном смысле: я могу отключить его, унести куда-то, подключить к интернету — и сервер по-прежнему со мной.

HTTP-приложения используют Cloudflare Tunnel. Cloudflared делает одно исходящее соединение с телефона, Cloudflare направляет каждый hostname через него, а Caddy маршрутизирует запрос на нужный loopback-сервис.

ingress
Internet
  → Cloudflare Tunnel
  → Caddy on 127.0.0.1
  → application on 127.0.0.1

Никакого inbound-правила на роутере для этих сервисов нет. Cloudflared нужен только исходящий коннект — если я перенесу телефон в другую сеть, туннель переподключится, и имена хостов последуют за ним. Tailscale делает то же самое для администрирования. Батарея телефона может перекрыть переезд, а публичные сервисы не заботит, какой Wi-Fi под ними.

🌐 Surf и роуминг
Бэкенд удалённого браузера Surf потребовал другого пути. Его прямое соединение чувствительно к задержкам, терминирует собственный TLS, а старый iPad, который к нему подключается, пиннит идентичность сервера. Дома Cloudflare DDNS держит DNS-only-запись, указывающую на текущий публичный адрес, а роутер форвардит один порт на Surf. В LAN iPad коннектится напрямую к телефону.

Для роуминга решение — обернуть весь Surf TLS-поток в обычный WebSocket. Cloudflare видит и форвардит WebSocket, но аутентифицированное соединение Surf остаётся зашифрованным end-to-end внутри него. Это добавляет ~один сетевой RTT задержки извне (около 60 мс при первом тесте), но работает через туннель, требующий только исходящего соединения, на ОС 2012 года и без установки Tailscale на iPad.

Это был момент, когда телефонная связка стала слегка нелепой — в лучшем смысле. Я оставил его включённым дома, ушёл в офис, подключился по SSH с MacBook через Tailscale к рабочей станции и телефону и использовал оригинальный iPad через Surf, хостящийся на телефоне. VPS больше не было — и это ощущалось освобождающе.

06Что на самом деле крутится

У машины два слоя. Android и Termux владеют железом, сетью, ingress'ом и супервизией. Linux-резиденты получают ожидаемую файловую систему и в остальном не мешают хосту.

Слой Ответственность Компоненты
Android / Termux (хост) железо, сеть, ingress, супервизия runit, Tailscale, Caddy, Cloudflared, DDNS, dashboard операций
рутированные Linux-резиденты совместимость приложений и рабочие нагрузки Surf и Chrome, Финансы, Screen Share, другие приложения

Самая требовательная нагрузка — Surf, который приносит современный Chromium-браузер на старые iPhone и iPad. Телефон крутит десктопный Chrome (свежий arm64-релиз) и бэкенд Surf внутри Debian-runtime; iPad mini получает H.264-видео и аудио, отправляя обратно тач, клавиатуру, табы и навигационные команды. Именно Surf — причина, почему я так заботился о накладных расходах сисколов и задержках.

Рядом телефон хостит мой личный финансовый трекер. Он записывает регулярные доходы и расходы, разовые транзакции, месячные лимиты трат и прогнозирует баланс по дням вперёд. В отличие от заменяемых артефактов приложений, его SQLite-база содержит состояние, которое мне дорого, поэтому есть автоматические бэкапы вне устройства и протестированный путь восстановления.

Ни одно из этих имён не встроено в какой-то грандиозный «фреймворк телефонного сервера». Новый резидент — это ещё один иммутабельный артефакт, определение runit, явный контракт данных, health-check и опционально маршрут Caddy.

Последнее добавление — наблюдаемость (observability). Я по-прежнему могу зайти по SSH, проверить runit, полистать логи, проверить публичные маршруты и опросить Android напрямую, но больше не нужно делать это только чтобы понять, что происходит с машиной: один нативный сервис собирает загрузку CPU всех восьми ядер, память, хранилище, аптайм, батарею, температуру, локальную и публичную доступность и каждого обнаруженного runit-резидента. Он хранит ограниченную историю и отдаёт встроенный Vue-интерфейс по адресу https://dash.cmf, доступный только в LAN или tailnet.

Системный дашборд телефона-сервера
Дашборд: CPU по ядрам, память, батарея, температура и статус всех сервисов

Просмотр логов обнаруживает те же директории сервисов в runtime, так что добавление нового резидента не требует обучать UI его имени. Это гораздо приятнее, чем заходить по SSH только чтобы вспомнить, кто шумел.

07Стоит ли вам это делать?

Если у вас уже есть достаточно современный, рутируемый ARM64-телефон, лежащий без дела — это гораздо менее нелепо, чем звучит. Вы получаете тихое железо, низкое энергопотребление, флэш-хранилище, Wi-Fi, встроенный дисплей для восстановления и батарею, которая работает как крошечный ИБП. Стоковый Android уже поддерживает железо, а Termux и рутированный chroot достаточно, чтобы запустить удивительное количество обычного Linux-софта.

Я бы не стал хранить незаменимые данные на таком устройстве без автоматических бэкапов вне телефона и не рассматривал бы chroot'ы как изоляцию враждебных нагрузок. Рутинг расширяет границу доверия, Android остаётся необычным серверным хостом, а софтверный рендеринг десктопного Chrome не обгонит выделенную рабочую станцию с GPU.

Но для набора личных сервисов — особенно когда альтернативой является бесконечная оплата VPS, который либо медленный, либо раздражающе дорогой — это действительно полезный вариант, а не просто трюк.

📌 Итог
Телефон остаётся странным сервером. Он делит одно ядро, Android иногда нужно напоминать, чтобы его не «оптимизировал», а будущий апдейт Android всегда может преподнести новый сюрприз.

Но он тихий, с батарейным резервом, достаточно быстрый, доступный отовсюду, воспроизводимый из Git — и уже лежит у меня дома. Я начал это, чтобы сэкономить на VPS. В итоге получил рутированный телефон с моей личной инфраструктурой — и как-то так это гораздо приятнее. :)
Postscript

Эта статья обсуждалась на Hacker News. В истинном духе HN удивительно большая часть обсуждения свелась к спору о заголовке. Мне понравился случайно случившийся лингвистический тред. :)