Mijn server is nu een telefoon
Een tijd lang draaide mijn persoonlijke infrastructuur op een kleine VPS van Hetzner. Hierop stonden een paar web-apps, een remote browser genaamd Surf, Caddy en de gebruikelijke ondersteunende software. Niets heel bijzonders; het werkte, maar ik vond het vervelend om er maandelijks voor te betalen.
Een van de apps die ik gebruik, Surf, maakte dit compromis lastig te negeren. De goedkoopste shared machines waren prima totdat Chrome echt aan het werk moest, waarna ze tekortschoten in rekenkracht. Machines met dedicated CPU's lossen dat op, maar de maandelijkse kosten maken een persoonlijke browser tot een twijfelachtige financiële investering.
Het kopen van een nieuwe machine was ook geen aantrekkelijke uitweg. De prijzen voor DRAM zijn volledig absurd geworden, waardoor het samenstellen van een nieuwe box met voldoende geheugen slecht getimed voelde. Ik keek naar gebruikte mini-pc's en overwoog kortstondig om mijn desktop als server te gebruiken wanneer ik hem niet gebruikte. Toen herinnerde ik me de CMF Phone 1 die ik al bezat.
Acht ARM-cores, 8 GB RAM, 128 GB flashgeheugen, Wi-Fi 6, een 5G-modem en een ingebouwde batterij-backup: dat is allemaal gekoppeld aan een SoC die overgekwalificeerd is om in een lade te liggen. En ik had er al voor betaald. Na hem af te stoffen en er een tijdje mee te spelen, besloot ik de telefoon om te toveren tot server.
Vandaag de dag draait erop Surf met een beheerde Chrome-instantie, mijn persoonlijke financiële tracker, een service voor schermdelen en een handvol kleinere web-apps. Ze overleven reboots, worden via Git gedeployed en blijven bereikbaar wanneer de telefoon tussen netwerken wisselt. Dit is nu dus de machine die de VPS daadwerkelijk heeft vervangen.
Het eerste slechte idee: Android vervangen
De schoonste versie van dit idee leek het flashen van een normale Linux-distributie. De CMF Phone 1 heeft een postmarketOS device port; hij boot, en de apparaatpagina bevat genoeg groene vinkjes om een roekeloos persoon optimistisch te maken. Ik bleek die persoon te zijn.
Waar ik minder aandacht aan besteedde, was alles wat als 'broken' was gemarkeerd: Wi-Fi, Bluetooth, hardwareversnelling en de meeste andere zaken die de telefoon nuttig maken als kleine server. Ik kwam tot het postmarketOS splashscreen en een zwart scherm. Op dat moment had ik noch een server, noch een telefoon.
Het herstellen van de stock Nothing OS werd een project op zich. De flashing utility vereiste Windows, dus installeerde ik Windows in QEMU, vocht ik met USB-passthrough en MediaTek-drivers, zag ik de flashing tool vastlopen en verplaatste ik het proces uiteindelijk naar een echte Windows-installatie om de fabrieksimages te herstellen. Er was een moment waarop de telefoon 'soft-bricked' was en alleen een zwart scherm toonde; ik dacht oprecht dat ik een perfect goed apparaat in een briefopener had veranderd.
Hij kwam terug, en de les was geleerd: Android heeft al werkende drivers voor elk onderdeel van deze hardware. Wi-Fi, energiebeheer, de batterij, de GPU, het modem en elk vreemd vendor-detail werken al. Alles dat weggooien in het streven naar een meer conventionele userspace was de verkeerde ruil.
Ik had er niet echt toe nodig dat de telefoon een normale Linux-machine werd. Ik had nodig dat hij Linux-applicaties betrouwbaar kon draaien, terwijl Android het hardware-specifieke werk bleef doen waar het goed in is.
Termux als host-besturingssysteem
Bij de tweede poging behield ik de stock Android en gebruikte ik Termux als host-omgeving.
Termux biedt mij OpenSSH, runit, Caddy, Cloudflared, pakketbeheer en voldoende standaard Unix-tooling. Termux:Boot start de supervisor en SSH na een reboot. Tailscale geeft de telefoon een stabiel privaat adres, zodat ik vanaf elke machine op mijn tailnet simpelweg kan draaien: ssh cmf.
Termux is geen virtuele machine. De processen worden nog steeds uitgevoerd tegen de Linux-kernel van Android, maar de Bionic-gebaseerde userspace verschilt genoeg van een gewone Debian-installatie dat bestaande Linux-applicatie-images niet zomaar kunnen worden geïmporteerd. Die splitsing bleek echter nuttig: Termux kon het kleine host-controlpaneel blijven, terwijl elke applicatie het Linux-bestandssysteem meebracht dat hij verwachtte.
De eigenlijke services worden beheerd door runit. Het batterijbeheer van Android is erg goed in zijn normale taak en erg slecht voor een apparaat dat zich voordoet als server. Daarom liet ik mijn Ansible-build ook een Android-hostprofiel toepassen: het installeert een persistente wake lock, schakelt light en deep idle uit, ontslaat Termux, Termux:Boot en Tailscale van achtergrondbeperkingen, schakelt de child process limiter uit, voorkomt Wi-Fi-suspendering en configureert Tailscale als de 'always on VPN'.
De recovery chain is belangrijker dan enige individuele instelling. Android boot → always on VPN brengt Tailscale terug → Termux:Boot start runit → runit start elke resident service → health checks verifiëren de lokale en publieke paden. De telefoon kan rebooten zonder dat ik hoeft te wachten tot ik het merk.
De opstartvolgorde:
- Android boot
- Tailscale always-on VPN
- Termux:Boot
- runit
- Resident services
- Lokale en publieke health checks
Dit is geen conventionele Linux-server. Er is geen systemd, geen normale Docker-daemon en er is geen reden om te doen alsof dat wel zo is. Maar het is een Linux-kernel met een zeer capabele userland daarop, en dat blijkt voldoende te zijn.
Het tweede slechte idee: PRoot
De meeste van mijn applicaties werden al geleverd als Linux ARM64 OCI-images. proot-distro maakte het verrassend eenvoudig om deze onder Debian te draaien zonder de applicaties zelf aan te passen.
PRoot onderschept bestandssysteem- en procesoperaties in de userspace en doet een regulier Termux-proces geloven dat het zich in een Debian root-bestandssysteem bevindt. Het is geen containergrens; alles deelt nog steeds de kernel van Android, de netwerknaamruimte en de Termux UID. Maar als compatibiliteitslaag voor applicaties is het extreem nuttig omdat het noch root, noch een speciale kernel nodig heeft.
De gewone webdiensten draaiden in eerste instantie prima op deze manier. Elke applicatie kreeg een geverifieerd root-bestandssysteem, een loopback-poort en een runit-service. Caddy draaide direct in Termux en routeerde hostnamen naar die poorten.
De performance- en latency-gevoelige workload van de Surf-browser was de uitzondering. Het starten van processen, het openen van libraries, het doorzoeken van paden, het lezen van browserprofielen en het verschuiven van capture-data liepen allemaal via de userspace-translatielaag van PRoot. Er was CPU beschikbaar, maar Chrome kon deze niet efficiënt bereiken. Daarom heb ik de telefoon geroot, niet om Android te vervangen, maar om hetzelfde Debian-bestandssysteem correct te mounten en er met een echte chroot in te stappen.
Runit beheerde nog steeds de lifecycle vanuit Termux, de configuratie kwam nog steeds van dezelfde plek en applicatiegegevens bleven in de Termux-opslag staan. De workload bereikte de kernel van Android simpelweg via native syscalls in plaats van PRoot. De verbetering was enorm!
Toen dat pad solide was, maakte het niet langer zin om kleinere services onder PRoot te laten draaien; ze draaien nu allemaal op dezelfde manier. Mijn workstation resolveert elk ARM64-image naar een exacte digest en exporteert het bestandssysteem; Ansible verifieert en installeert dit op de telefoon. Een kleine root-helper creëert een private mount namespace, bindt de vereiste paden, stapt met chroot het bestandssysteem binnen, verlaagt de privileges en start het originele image entrypoint.
Er hoeft geen Docker of compiler op de telefoon te staan. Dit zijn nog steeds compatibiliteitsomgevingen in plaats van beveiligingsgrenzen: de residents delen de kernel en netwerkstack van Android, terwijl de private mount namespaces vooral zorgen dat mounts en clean-up voorspelbaar blijven.
Ik heb ook veel te veel tijd besteed aan het proberen te overbruggen van Debian's graphics stack naar de Mali GPU van de telefoon via VirGL en Android Vulkan. Ik kreeg hardware-compositing vinkjes, maar ook corrupte pagina's en slechtere prestaties... Het saaie software-rendered pad bleek uiteindelijk beter te presteren.
Infrastructuur! Geen stapel shell-geschiedenis
Tegen die tijd kon de telefoon alles draaien, maar ik wilde geen 'huisdier-server' die was samengesteld uit commando's die ik over een week vergeten zou zijn. Ik heb de gehele host omgezet naar een door Ansible beheerde staat: versies, servicedefinities, routes, energie-instellingen, secrets en health checks leven allemaal in één privaat repository.
De deployment flow is ongeveer als volgt: release of OCI image → checksum/digest gepind in Git → Ansible over SSH → versioneerde bestanden op de telefoon → atomaire current symlink → runit service → lokale health check → publieke edge check.
Releases zijn gepind via digest of checksum en worden geïnstalleerd in versioneerde mappen achter een atomaire current symlink. Een mislukte checksum of health check stopt de deployment; rollback betekent het terugdraaien van de pin en opnieuw toepassen. Applicatiegegevens leven apart van de releases.
Na een kleine handmatige bootstrap (installeer de drie Android-apps, root de telefoon, geef Termux superuser-toegang en autoriseer SSH), beheert hetzelfde repository de rest. Vanaf mijn workstation is het brengen van de host naar de gedeclareerde staat bewust simpel:
make phonemake phone-statusmake phone-edge-check
Het opnieuw toepassen vervangt geen ongewijzigde runtime-bestanden of herstart gezonde residents. Belangrijker is dat de configuratie nuttig blijft als deze telefoon kapot gaat: een andere rootbare ARM64-telefoon kan naar dezelfde staat worden gebracht zonder een shell-geschiedenis te hoeven reconstrueren.
Secrets worden niet opgeslagen in de Git checkout op de telefoon, omdat er geen Git checkout is. Ansible Vault-waarden leven versleuteld in het infrastructuur-repository. Het vault-wachtwoord wordt afgeleid door mijn 1Password SSH-agent te vragen een vaste challenge te signeren, zodat de private key in 1Password blijft en de telefoon er nooit toegang toe nodig heeft. Tijdens deployment rendert Ansible alleen de runtime-waarden die elke service nodig heeft in de private opslag van Termux.
Verkeer naar een telefoon achter een thuisnetwerk krijgen
Het volgende probleem was ingress. Mijn thuisverbinding beschikt niet over de statische serversetup die een VPS biedt, en ik wil geen SSH of een verzameling willekeurige applicatiepoorten via de router blootstellen. Daarnaast wilde ik dat de telefoon in één belangrijk opzicht een telefoon bleef: ik moet hem kunnen loskoppelen, ergens anders naartoe meenemen, verbinden met internet en mijn server nog steeds hebben.
De HTTP-applicaties maken gebruik van een Cloudflare Tunnel. cloudflared maakt één uitgaande verbinding vanaf de telefoon, Cloudflare stuurt elk hostname daardoorheen en Caddy routeert het verzoek naar de juiste loopback-service.
Dataflow: Internet → Cloudflare Tunnel → Caddy op 127.0.0.1 → applicatie op 127.0.0.1
Er is geen inkomende routerregel voor deze services. Cloudflared heeft alleen een uitgaande verbinding nodig, dus als ik de telefoon naar een ander netwerk verplaats, herstelt de tunnel de verbinding en volgen de hostnames hem. Tailscale doet hetzelfde voor administratie. De batterij van de telefoon overbrugt de verplaatsing en de publieke services merken niet welk Wi-Fi-netwerk eronder ligt.
De Surf remote browser backend had echter een ander pad nodig. De directe verbinding is latency-gevoelig, termineert zijn eigen TLS, en de oude iPad die ermee verbindt, pinst de serveridentiteit. Thuis houdt Cloudflare DDNS een DNS-only record aan dat wijst naar het huidige publieke adres en de router stuurt één poort door naar Surf. Op het LAN verbindt de iPad direct met de telefoon.
Dat liet echter roaming onopgelost. Een normale Cloudflare Tunnel termineert TLS bij Cloudflare, wat precies is wat de gepinde verbinding van Surf niet wil. De oplossing was om de complete Surf TLS-stream in een gewone WebSocket te wikkelen. Cloudflare ziet en stuurt de WebSocket door, maar de eigenlijke geauthenticeerde Surf-verbinding blijft end-to-end versleuteld daarbinnen.
Dit voegt overduidelijk latency toe; van buiten het huis merk ik meestal ongeveer één extra netwerk-roundtrip, en de iPad-verbinding was rond de 60 ms bij de eerste test. Maar het werkt via een tunnel die alleen uitgaande connectiviteit nodig heeft, op een besturingssysteem uit 2012, zonder Tailscale op de iPad te installeren.
Dat was het moment waarop de telefoonsetup op de best mogelijke manier absurd werd. Ik liet hem thuis aangesloten, ging naar kantoor, SSH'te vanaf mijn MacBook via Tailscale naar mijn workstation en de telefoon, en gebruikte de originele iPad via de op de telefoon gehoste Surf-instantie. De VPS was weg; het voelde bevrijdend.
Wat er precies draait
De machine heeft twee lagen. Android en Termux beheren de hardware, netwerking, ingress en supervisie. De Linux residents krijgen het bestandssysteem dat ze verwachten en blijven verder uit de weg van de host.
| Laag | Verantwoordelijkheid | Onderdelen |
|---|---|---|
| Android / Termux host | Hardware, netwerking, ingress, supervisie | runit, Tailscale, Caddy, Cloudflared, DDNS, operations dashboard |
| Rooted Linux residents | Applicatie-compatibiliteit en workloads | Surf and Chrome, Finances, Screen Share, diverse andere apps |
De meest veeleisende workload is Surf, die een moderne Chromium-browser brengt naar oude iPhones en iPads. De telefoon draait desktop Chrome (een recente arm64 release) en de Surf backend binnen zijn Debian runtime; een iPad mini ontvangt H.264 video en audio terwijl hij touch-, toetsenbord-, tab- en navigatiecommando's terugstuurt. Surf is de reden waarom ik zoveel waarde hechtte aan syscall overhead en latency.
Daarnaast host de telefoon mijn persoonlijke financiële tracker. Deze registreert recurrente inkomsten en uitgaven, eenmalige transacties, maandelijkse bestedingslimieten en projecteert mijn saldo dag voor dag. In tegenstelling tot de vervangbare applicatie-artefacten bevat de SQLite-database status die ik belangrijk vind, dus heeft deze geautomatiseerde off-device backups en een geteste restore-route.
Geen van deze namen is ingebakken in een groots "phone server framework". Een nieuwe resident is simpelweg een ander immutable artefact, een runit-definitie, een expliciet datacontract, een health check en optioneel een Caddy-route.
De nieuwste toevoeging is observability. Ik kan nog steeds via SSH inloggen, runit inspecteren, logs tailen, publieke routes controleren en Android direct bevragen, maar ik hoef dat niet meer te doen om in één oogopslag te zien wat er met de machine aan de hand is: een native service verzamelt CPU-gebruik voor alle acht cores, geheugen, opslag, uptime, batterij, thermals, lokale en publieke bereikbaarheid, en elke ontdekte runit resident. Het houdt een beperkte historie bij en serveert een embedded Vue interface op https://dash.cmf, alleen bereikbaar via het LAN of tailnet.
De log-view ontdekt dezelfde servicedirectories tijdens runtime, zodat het toevoegen van een nieuwe resident niet betekent dat ik de UI zijn naam moet leren. Dit is veel prettiger dan inloggen via SSH om je te herinneren welk proces lawaai maakte.
Zou je dit moeten doen?
Als je al een redelijk moderne, rootbare ARM64-telefoon hebt liggen die ongebruikt is, is dit veel minder absurd dan het klinkt. Je krijgt stille hardware, een laag stroomverbruik, flash-opslag, Wi-Fi, een ingebouwd display voor recovery en een batterij die zich gedraagt als een kleine UPS. Stock Android ondersteunt de hardware al, terwijl Termux en een rooted chroot voldoende zijn om een verrassende hoeveelheid normale Linux-software te draaien.
Ik zou geen onvervangbare data op zo'n apparaat zetten zonder geautomatiseerde off-device backups, en ik zou de chroots niet behandelen als vijandige workload-isolatie. Rooten vergroot de trust boundary, Android blijft een ongebruikelijke serverhost, en software-rendered desktop Chrome gaat een dedicated workstation met een GPU niet verslaan.
Maar voor een handvol persoonlijke services, vooral wanneer het alternatief is om onbeperkt te betalen voor een VPS die ofwel traag of irritant duur is, is het een echt nuttige optie in plaats van alleen een stunt.
De telefoon is nog steeds een vreemde server. Hij deelt één kernel, Android moet af en toe herinnerd worden om hem niet te "optimaliseren", en een toekomstige Android-update kan altijd voor nieuwe verrassingen zorgen.
Maar hij is stil, batterij-backed, snel genoeg, overal bereikbaar, reproduceerbaar via Git en bevindt zich al in mijn huis. Ik begon hiermee om geld te besparen op een VPS. Ik eindigde met een gerootte telefoon die mijn persoonlijke infrastructuur draait, en op somehow is dat veel bevredigender. :)
Groetjes,