Once I had the lighting web interface working, I wanted it available whenever I was at home, from any device around the house.
The interface itself is static HTML, CSS, and JavaScript. Once loaded, the browser communicates directly with the ESP8266 controllers through the WLED API. So keeping the interface available does not require an application server or much computing power. It only needs something on the local network that can stay on and serve the files.
I had a few old Android phones lying around and started wondering whether one of them could do that job. I found Termux, installed it on an unused Redmi Note 12, and turned the phone into the local server for the interface.

How it works
ESP8266 controllers
▲
│ WLED API
│
Browser running the web interface
▲
│ HTTP :4321
│
Redmi Note 12
Android + Termux
├── serve
├── sshd
└── ~/lights-dashboard/
▲
│ rsync over SSH :8022
│
Linux development machine
Astro source
└── pnpm deploy
Development stays on my Linux machine. Running pnpm deploy builds the Astro project there and synchronizes the contents of dist/ to the phone over SSH.
The phone stores those production files in ~/lights-dashboard/ and serves them over HTTP on port 4321. A browser loads the interface from the phone and then talks directly to the ESP8266 controllers through the WLED API.
The phone is therefore not a central lighting controller. If the server becomes unavailable, the shared web interface disappears, but the ESP8266 controllers remain available through their individual WLED interfaces.
Server setup
The server on the phone is a small Termux environment with two long-running services.
sshd provides remote access and the SSH connection used during deployment. serve is the HTTP server that exposes the built interface to the local network. Both run under termux-services, which uses runit to supervise them.
rsync is also installed on the phone, but it is not another server process. It is used when a new build is synchronized from the development machine.
The required packages are installed inside Termux with:
pkg update
pkg upgrade
pkg install openssh nodejs-lts npm rsync termux-services
npm install -g serve
openssh provides sshd. Node.js and npm are needed because serve is distributed as an npm package. termux-services provides the service supervision used for both sshd and serve.
After installing termux-services, I restart Termux so its service environment is initialized.
The server does not contain the Astro project. It only needs a directory for the finished production files:
mkdir -p ~/lights-dashboard
That directory starts empty and is populated by deployment from the development machine.
Network address and SSH
Because the browser URL, SSH connection, and deployment script all refer to the phone by its local address, I reserve that address in the router rather than letting it change with normal DHCP assignment.
I use 192.168.0.12 for the reserved address.
Termux runs its SSH server on port 8022. I set a password for the Termux user and enable sshd under termux-services:
passwd
sv-enable sshd
sv status sshd
Once it is running, I can connect from the development machine with:
ssh -p 8022 192.168.0.12
That gives me normal terminal access to the phone and also provides the transport used later by rsync.
HTTP service
The web interface is served by a second supervised service.
I created a service directory called dashboard:
mkdir -p $PREFIX/var/service/dashboard
Its run script, $PREFIX/var/service/dashboard/run, starts serve with the deployed directory as its document root:
#!/data/data/com.termux/files/usr/bin/sh
exec serve "$HOME/lights-dashboard" -l 4321
serve listens on port 4321 and returns the files stored in ~/lights-dashboard/.
The exec is significant here. It replaces the shell with the serve process, so runit supervises the HTTP server itself rather than a shell that happens to have launched it.
I then make the script executable and enable the service:
chmod +x $PREFIX/var/service/dashboard/run
sv-enable dashboard
sv status dashboard
Once a build has been deployed, the interface is available to devices on the local network at:
http://192.168.0.12:4321
Deployment
Astro is built on the development machine, while the phone only receives and serves the finished files.
I wrapped the build and transfer into scripts/deploy.sh:
#!/usr/bin/env bash
set -e
pnpm build
rsync \
-az \
--delete \
-e "ssh -p 8022" \
dist/ \
192.168.0.12:~/lights-dashboard/
set -e stops the script immediately if pnpm build fails, so a failed build cannot continue into deployment.
The rsync command connects through Termux’s SSH server on port 8022 and synchronizes the contents of dist/ into ~/lights-dashboard/.
--delete removes files on the phone that disappeared from the new build. That is useful with Astro because generated assets use hashed filenames; without it, assets from older builds would accumulate.
The trailing slash on dist/ is also intentional. It copies the contents of that directory into ~/lights-dashboard/ rather than creating ~/lights-dashboard/dist/.
The project exposes the deployment script through package.json:
{
"scripts": {
"deploy": "./scripts/deploy.sh"
}
}
A normal deployment is therefore:
pnpm deploy
The running HTTP service does not need to restart afterward. serve continues using the same directory and serves the newly synchronized files.
Keeping the source and build environment off the phone also leaves very little state tied to the device. Replacing it would mainly mean recreating the Termux services and deploying the current build again.
Keeping the server running
The actual serving workload has not been a problem. But Android’s process management has.
I configured Termux with Battery usage: No restrictions and enabled Background autostart. I also acquire a Termux wake lock:
termux-wake-lock
The wake lock allows Termux to continue doing work while the screen is off. It does not keep the display itself awake.
On this Xiaomi phone, those settings still did not make the server reliable when Termux was left as a background application.
What has worked reliably is leaving Termux as the foreground app before locking the phone. The screen can then turn off normally while the services continue running.
This also defines the limit of the service supervision. runit can restart sshd or serve if either process exits, but it cannot restart anything if Android kills the entire Termux environment that contains runit.
That is the main compromise in using the phone this way.
Reboot behavior
The current setup does not automatically recover from a full Android reboot.
After restarting the phone, I unlock it and open Termux again. Once the Termux environment is running, its enabled services can be supervised normally.
Termux has a separate Termux:Boot add-on that can launch scripts during Android startup, but I am not using it in this setup.
The distinction matters because placing a script under:
~/.termux/boot/
does not make it run automatically. Those scripts are executed by the Termux:Boot application, so the directory alone does nothing without that add-on installed and configured.
Operation and maintenance
Under normal use, I rarely need to interact with the phone directly. Changes to the interface are built and deployed from the development machine with:
pnpm deploy
If I need to inspect the server itself, I connect over SSH:
ssh -p 8022 192.168.0.12
From there, the two long-running services can be checked with:
sv status dashboard
sv status sshd
If the HTTP server itself needs restarting:
sv restart dashboard
Deploying new static files does not normally require that restart.