Vithos needed an always-on backend, but a VPS is a monthly bill for a side project. A spare Android phone draws little power and has a built-in battery as backup, but it sits behind a home router, often on CGNAT, and Android keeps trying to stop background apps.
- 01The API runs in Termux on the phone. Cloudflare Tunnel keeps an outbound connection to Cloudflare's edge and forwards a public hostname to localhost, so no inbound ports are needed.
- 02Every push to main triggers GitHub Actions, which POSTs to a small Node.js webhook guarded by a shared secret compared in constant time. The webhook runs the deploy script and returns its exit status and output, so a failed deploy fails the CI job.
- 03The deploy script stops on the first error and builds before restarting, so a broken build never replaces the running API. tmux keeps the API, tunnel and webhook alive, and Termux:Boot recreates them after a reboot.
- 04The first listener was the adnanh/webhook Go binary, which crashed with SIGSYS on Android: the Go runtime's process-spawning path hit Android's syscall restrictions. It was replaced with a dependency-free Node.js server that lives in the repo.
Architecture
- 1PushA push to main starts a GitHub Actions job
- 2WebhookAuthenticated POST /deploy through the tunnel
- 3Deploygit pull, pnpm install, pnpm build, then restart
- 4ServeAPI on the phone, public via Cloudflare Tunnel
- 5OperateSSH over Tailscale, /stats telemetry, UptimeRobot
- Public HTTPS through an outbound Cloudflare Tunnel, so it works behind CGNAT with no ports opened on the router.
- Push-to-deploy: GitHub Actions calls an authenticated webhook that pulls, installs, builds and restarts the API, and gets the real exit status and output back.
- Builds before restarting, so a failed install or build leaves the running version untouched.
- Survives reboots: Termux:Boot takes a wake lock and recreates the tmux sessions for the API, tunnel and webhook.
- Authenticated /stats endpoint reports battery, storage, memory and load from the phone; SSH is reachable only over Tailscale.
