- Published on
遅かったのはCPUではなくシステムコールの経路だった — スマホをサーバーにして学ぶこと
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — VPSを解約しようとして始まった実験
- 一度目の失敗 — ドライバを捨ててLinuxを得ようとする
- Termuxをホストの制御プレーンとして使う
- Androidの電源管理と戦う項目一覧
- PRootとchroot — コストはCPUではなくシステムコールに付く
- 互換レイヤは隔離境界ではない
- イングレス — アウトバウンド接続ひとつで作る公開サービス
- 再現可能性 — シェルの履歴ではなくGit
- 参考資料
はじめに — VPSを解約しようとして始まった実験
2026年8月4日、seg6.spaceにmy server is a phone nowという記事が上がりました。Hetzner VPSで動かしていた個人向けサービスを、引き出しにあったCMF Phone 1へ移した記録です。
スマホのスペックはARMコア8個、RAM 8GB、フラッシュ128GB、Wi-Fi 6、5Gモデム、そして内蔵バッテリーです。最後の項目がさりげなく重要で、サーバーの立場からすると小さな無停電電源装置だからです。
この記事が読む価値のある理由は「スマホでサーバーを回した」という結果ではなく、著者が二度失敗してその失敗から正確な教訓を引き出したことにあります。特に二度目の失敗は、Linuxのコンテナと互換レイヤを扱う人なら誰にでも当てはまる話です。
一度目の失敗 — ドライバを捨ててLinuxを得ようとする
いちばんきれいに見えるアプローチは、普通のLinuxディストリビューションをフラッシュすることです。CMF Phone 1にはpostmarketOSのデバイスポートがあり、起動もし、デバイスページには緑の表示が十分たくさんあります。
著者が注意深く見なかったのは「壊れている」と表示された項目でした。Wi-Fi、Bluetooth、ハードウェアアクセラレーション。結果はpostmarketOSのスプラッシュ画面と真っ黒なディスプレイであり、その時点でサーバーもスマホも無い状態になりました。
復旧も簡単ではありませんでした。フラッシュツールがWindowsを要求するのでQEMUにWindowsを入れ、USBパススルーとMediaTekのドライバと格闘し、結局実機のWindowsマシンで工場出荷イメージを復元しました。途中でソフトブリック状態になり黒い画面しか出ない区間もありました。
著者が引き出した教訓は正確です。Androidにはこのハードウェアのすべての部品に対する動作するドライバがすでにあります。 Wi-Fi、電源管理、バッテリー、GPU、モデム、あらゆるベンダー固有の細部まで全部です。より馴染みのあるユーザースペースを得るためにそれを丸ごと捨てるのは悪い取引でした。
必要だったのはスマホが普通のLinuxマシンになることではなく、Androidがハードウェア関連の仕事を続けているあいだにLinuxアプリケーションを安定して回すことでした。
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の電源管理と戦う項目一覧
スマホをサーバーとして使うとき、もっとも厄介な相手はCPUではなくバッテリー最適化です。著者の表現では、Androidのバッテリー管理は本来やるべきことにはとても優れており、サーバーのふりをする機器にはとても悪い、です。
Ansibleのビルドが適用するAndroidホストプロファイルは次のことをします。
- 持続的なウェイクロックの設置
- 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権限も特別なカーネルも要らないという点が大きな長所です。
一般的なウェブサービスはこの方式でよく回りました。アプリケーションごとに検証済みのルートファイルシステム、ループバックのポートひとつ、runitのサービス定義を与え、Caddyがホスト名をそのポートへルーティングします。
例外がひとつありました。Surfという遠隔ブラウザのワークロードです。プロセスの起動、ライブラリのオープン、パスの走査、ブラウザプロファイルの読み取り、キャプチャデータの移動が全部PRootのユーザースペース変換レイヤを通過しました。著者の文が正確です。CPUは余っていたが、Chromeがそこへ効率的に到達できなかった、と。
この文は噛みしめる価値があります。監視ダッシュボードにはアイドルのCPUが見えます。メモリも余ります。ところがアプリケーションは遅い。ボトルネックが資源の量ではなく資源に到達する経路にあるからです。
解決はコードを直すことではありませんでした。スマホをroot化して、Androidを置き換えるためではなく、同じDebianファイルシステムをきちんとマウントして本物のchrootへ入るためにroot化しました。runitは依然としてTermuxでライフサイクルを管理し設定もそのままなのに、ワークロードだけが変換レイヤの代わりにネイティブなシステムコールでカーネルに到達します。著者の表現で「改善は微妙なものではありませんでした」。
一般化するとこうです。互換レイヤのコストは計算量ではなくシステムコールの頻度に比例します。 大きな配列を回る計算中心のプログラムはPRootの上でもほとんど損がありません。ファイルを数千個開きプロセスを立て続けに起動するプログラムは同じレイヤで崩れます。どのワークロードがどちら側かをあらかじめ知っておくことが、この判断のすべてです。
互換レイヤは隔離境界ではない
性能の話のあとに著者が釘を刺す文があり、この部分は引用する価値があります。
PRootについて「これはコンテナ境界ではない」と書きます。すべてが依然としてAndroidのカーネル、ネットワークネームスペース、TermuxのUIDを共有します。アプリケーションの互換レイヤとしては極めて有用ですが、それが全部です。
chrootへ移ったあとも同じ態度を維持します。デプロイの流れはこうです。作業用コンピュータが各ARM64イメージを正確なダイジェストで解決してファイルシステムを抽出し、Ansibleが検証したあとスマホにインストールします。小さなrootヘルパーが私設のマウントネームスペースを作り必要なパスをバインドしchrootへ入ったあと権限を下げ、元のイメージのエントリポイントを実行します。スマホにはDockerもコンパイラも要りません。
そしてこう付け加えます。これらは依然としてセキュリティ境界ではなく互換環境であり、私設のマウントネームスペースは主にマウントと後片付けを予測可能に保つための用途だ、と。
この区別はスマホの外でも有効です。コンテナを隔離と呼ぶ習慣のせいで、私たちはしばしばネームスペースが与えるものとサンドボックスが与えるものを混ぜて考えます。マウントネームスペースはファイルシステムのビューを分けます。カーネルを分けはしません。敵対的なワークロードを同じカーネルの上に載せてネームスペースだけで防ごうと考えているなら、それは別の設計が必要な問題です。
イングレス — アウトバウンド接続ひとつで作る公開サービス
家庭用回線には固定IPも無く、ルーターにSSHやアプリケーションのポートを開けたくもありません。しかも著者はスマホを持って出かけてもサーバーが生き続けることを望みました。
HTTPサービスはCloudflare Tunnelを使います。cloudflaredがスマホから外へ接続をひとつ作り、Cloudflareがホスト名ごとにその通路へリクエストを押し込み、Caddyがループバックのサービスへルーティングします。
Internet -> Cloudflare Tunnel -> Caddy on 127.0.0.1 -> application on 127.0.0.1
ルーターにインバウンドの規則がありません。だからスマホを別のネットワークへ移すとトンネルが再接続され、ホスト名がそのままついてきます。管理用のアクセスはTailscaleが同じ仕事をします。
ひとつのサービスだけ別の経路が必要でした。Surfのバックエンドは遅延に敏感で、自前でTLSを終端し、接続してくる古いiPadがサーバーの身元をピン留めします。一般的なCloudflare TunnelはTLSをCloudflareで終端しますが、ピン留めされた接続にはそれが即座に失敗を意味します。
解法はSurfのTLSストリーム全体を普通のWebSocketのなかに包むことでした。CloudflareはWebSocketを見て転送するだけであり、実際の認証された接続はそのなかで端から端まで暗号化されたまま残ります。代償は遅延です。家の外ではおおよそネットワークの往復がひとつ増え、iPadの接続は最初に測ったとき60ミリ秒ほどだったと書かれています。
TLSを終端するプロキシと身元をピン留めするクライアントが出会うと、いつもこの問題が起きます。包むことはその衝突を避ける標準的な回避策であり、コストは往復ひとつです。
再現可能性 — シェルの履歴ではなくGit
最後に運用の観点からの教訓です。
著者は「一週間で忘れてしまうコマンドで組み立てられたペットのサーバー」を望みませんでした。だからホストの状態全体を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
リリースはダイジェストやチェックサムで固定されバージョンディレクトリにインストールされ、原子的なcurrentシンボリックリンクの後ろに置かれます。チェックサムやヘルスチェックが失敗すればデプロイが止まり、ロールバックは固定値を戻して再適用することです。アプリケーションのデータはリリースと分離されています。
シークレットの扱いも注目に値します。スマホにはGitのチェックアウトが無いのでシークレットもありません。Ansible Vaultの値はインフラのリポジトリに暗号化されたまま置かれ、vaultのパスワードは1Password SSHエージェントに固定されたチャレンジへ署名させて導出します。秘密鍵は1Passwordのなかに残り、スマホはそれにアクセスする必要がありません。デプロイの時点でAnsibleが、各サービスに必要なランタイム値だけをTermuxの私設ストアへレンダリングします。
著者自身が付けた但し書きもそのまま移しておきます。自動のオフデバイスバックアップ無しに代替不可能なデータをここに置かないと言い、chrootを敵対的ワークロードの隔離として扱わないと言い、root化が信頼境界を広げる点と今後のAndroidの更新が新しい問題を作りうる点を認めます。良い実験記録の条件は、これらの但し書きが結論と同じ大きさで書かれていることです。
参考資料
- my server is a phone now — seg6.space, 2026-08-04 (ハードウェアのスペック、設定の一覧、遅延の数値は全部この記事から移したものです)
- PRootプロジェクト
- Termuxウィキ — Termux:Boot
- Cloudflare Tunnelドキュメント
- Hacker News討論スレッド