← All Notes

Hosting the WLED Dashboard on Android with Termux

Purpose

I have been building several home lighting installations using WS2812B LED strips, Wemos D1 Mini / ESP8266 controllers, and WLED.

Each WLED controller provides its own web interface, but once there are several lights around the house, opening and controlling every device individually becomes inconvenient.

To solve that, I built a local Astro dashboard that provides one interface for controlling all the WLED devices.

The dashboard originally ran from my Ubuntu development computer. That worked while the computer was running, but the dashboard disappeared whenever the computer was turned off.

I therefore needed a small machine that could remain on the local network and serve the dashboard independently.

Instead of buying additional hardware, I repurposed an unused Redmi Note 12 Pro as the server.

The phone does not contain or build the Astro project. Development remains on Ubuntu. The phone only receives the finished production files and makes them available over the local network.

System design

                         LOCAL NETWORK

┌─────────────────────────────┐
│ WLED devices                │
│                             │
│ ESP8266 / Wemos D1 Mini     │
│ + WS2812B installations     │
│                             │
│ desk-monitor.local          │
│ desk-back.local             │
│ shelves, wardrobe, etc.     │
└──────────────▲──────────────┘

               │ WLED HTTP API

┌──────────────┴──────────────┐
│ Browser                     │
│                             │
│ WLED Dashboard              │
│                             │
│ http://SERVER_IP:4321       │
└──────────────▲──────────────┘

               │ HTTP :4321

┌──────────────┴──────────────┐
│ Redmi Note 12 Pro           │
│                             │
│ Android                     │
│ └── Termux                  │
│     ├── serve               │
│     ├── sshd                │
│     └── termux-services     │
│         └── runit           │
│                             │
│ ~/lights-dashboard/         │
└──────────────▲──────────────┘

               │ SSH :8022
               │ rsync

┌──────────────┴──────────────┐
│ Ubuntu workstation          │
│                             │
│ Astro source                │
│ pnpm build                  │
│ dist/                       │
└─────────────────────────────┘

The responsibilities are deliberately separated.

Ubuntu contains the dashboard source code. This is where the dashboard is developed, tested, and built.

The Android phone contains only the production build. Its job is to serve those files, provide remote SSH access, and keep the required server processes running.

The WLED controllers remain responsible for the actual lights. The dashboard communicates with them over the local network through the WLED HTTP API.

This means the dashboard server is only a centralized control layer. If the Android phone is unavailable, the WLED controllers and their individual interfaces continue to work.

Design decisions

Why use an old Android phone?

The phone was already available, consumes relatively little power, has built-in Wi-Fi and battery backup, and is far more powerful than necessary for serving a small static website.

Using it avoids buying a Raspberry Pi, mini PC, or other dedicated server just for the dashboard.

Why Termux?

Android does not normally provide the Unix command-line environment needed to run tools such as an SSH server or rsync.

Termux provides that environment without replacing Android or requiring a conventional Linux installation.

Why serve?

An Astro production build consists of static files such as HTML, CSS, JavaScript, and images.

The phone therefore does not need an application framework or complicated web server. It only needs something that can expose a directory of files over HTTP.

serve is a small Node.js static HTTP server that does exactly that.

Why SSH?

SSH lets me manage Termux remotely from my Ubuntu computer.

Instead of entering commands through the phone’s touchscreen keyboard, I can open a normal Ubuntu terminal and connect directly to the Termux shell over the LAN.

SSH is also used by the deployment process to securely transfer new dashboard builds to the phone.

Why rsync?

Updating the dashboard is essentially a directory synchronization task.

rsync transfers only files that changed and can remove old generated files that no longer exist in the current build.

That makes it better suited to repeated deployments than manually copying the entire build every time.

Why termux-services and runit?

The SSH server and dashboard server are both programs that need to remain running for long periods.

Starting them manually would work, but if either process exits, nothing would automatically bring it back.

termux-services provides service management inside Termux using a small process supervisor called runit.

Its responsibility is essentially:

runit
├── sshd
│   └── keep the SSH server running

└── dashboard
    └── keep the HTTP server running

A program managed this way is called a supervised service.

Requirements

This setup assumes:

  • an Android phone connected to the home LAN
  • Termux installed
  • an Ubuntu/Linux development computer
  • an Astro WLED dashboard already working locally
  • the phone and WLED devices are on the same network
  • access to the router to reserve an IP address for the phone

The guide uses these example values:

Server IP:       SERVER_IP
SSH port:        8022
Dashboard port:  4321
Deploy directory:
~/lights-dashboard

SERVER_IP should be replaced with the reserved LAN address of the Android phone.

Setup

1. Update Termux

Before installing anything, update Termux’s package information and any packages that are already installed:

pkg update
pkg upgrade

pkg update refreshes the list of software available from the Termux repositories.

pkg upgrade then updates the packages already installed on the phone.

Doing this first gives the server a current and consistent environment before adding the tools it needs.

During a large upgrade, Termux may ask whether to keep an existing configuration file or replace it with the package maintainer’s version. If it is a file I never intentionally customized, I use the current package-maintainer version.

2. Allow Termux to run in the background

This phone is going to run a server while its screen is off, which conflicts with Android and Xiaomi’s normal goal of reducing background activity to save battery.

In Android’s settings for Termux, configure:

Battery usage:
No restrictions

Background autostart:
Enabled

These settings remove the normal restrictions that would otherwise work against a long-running server.

They do not completely solve Xiaomi’s background-process behavior on this particular phone. There is one additional practical requirement described later under Keeping Termux alive.

Xiaomi’s separate privacy feature called App Lock is unrelated to this setup.

3. Install the required server tools

The phone needs tools for remote access, serving the dashboard, receiving deployments, and managing long-running processes.

Install them together:

pkg install openssh nodejs-lts npm rsync termux-services

The packages have separate responsibilities:

openssh provides SSH, including sshd, the SSH server that lets Ubuntu connect remotely to Termux.

nodejs-lts provides the Node.js runtime required by the static HTTP server.

npm is Node.js’s package manager. It is only needed here to install the serve command.

rsync synchronizes new dashboard builds from Ubuntu to the phone.

termux-services provides service management using runit, which will keep the SSH and dashboard servers running.

After installing termux-services, close and reopen the Termux shell once so its service-management environment is initialized.

4. Install the static HTTP server

The dashboard is a static Astro build, so the phone only needs a small HTTP server that can expose a directory to the network.

Install serve globally:

npm install -g serve

The -g flag installs it globally inside Termux rather than inside a particular Node.js project.

This makes the serve command available anywhere in the Termux environment.

Later it will serve:

~/lights-dashboard/

which will contain the production files generated by Astro.

5. Reserve the phone’s LAN address

The server needs a stable address.

Normally the router assigns addresses using DHCP, which means the phone’s IP could eventually change. If that happened, the dashboard bookmark, SSH connection, and deployment script would all point to the wrong address.

Find the phone’s current IP address in Android’s Wi-Fi connection details and create a DHCP/IP reservation for it in the router.

For the rest of the guide, that address is represented as:

SERVER_IP

For example:

192.168.0.120

6. Configure remote SSH access

First create a password for the Termux user:

passwd

This password is used to authenticate SSH connections from Ubuntu.

Next, enable the SSH server as a supervised service:

sv-enable sshd

sshd means SSH daemon. It is the program that listens for incoming SSH connections.

sv-enable comes from termux-services. Instead of simply starting sshd once, it registers it as a service that runit should manage.

Check that it is running:

sv status sshd

A running service should show something beginning with:

run: sshd:

Termux’s SSH server listens on port 8022, so from Ubuntu I can now connect with:

ssh -p 8022 SERVER_IP

For example:

ssh -p 8022 192.168.0.120

-p 8022 tells SSH to connect to port 8022 instead of its normal default port 22.

Once this works, most of the remaining phone configuration can be done from the Ubuntu terminal rather than directly on the phone.

7. Create the dashboard directory

The production dashboard needs a permanent location on the phone.

Create it from Termux:

mkdir -p ~/lights-dashboard

mkdir creates a directory.

-p allows the command to run safely even if the directory already exists.

~ refers to the Termux user’s home directory.

The result is:

~/lights-dashboard/

This directory will eventually contain files such as:

index.html
_astro/
images/
...

The deployment flow will be:

Ubuntu
Astro project

    │ pnpm build

  dist/

    │ rsync

Android
~/lights-dashboard/

8. Run the dashboard as a supervised service

The dashboard server should remain running just like the SSH server.

Instead of starting serve manually whenever Termux starts, create a service definition for it so runit can manage it.

First create a directory for the new service:

mkdir -p $PREFIX/var/service/dashboard

$PREFIX points to Termux’s installation directory.

termux-services looks under:

$PREFIX/var/service/

for services it should manage.

The new directory therefore defines a service named dashboard.

Next, create the script that tells runit how to start that service:

nano $PREFIX/var/service/dashboard/run

Inside nano, add:

#!/data/data/com.termux/files/usr/bin/sh

exec serve "$HOME/lights-dashboard" -l 4321

The first line specifies the Termux shell that should run the script.

The second line starts serve using:

$HOME/lights-dashboard

as the directory to expose.

The option:

-l 4321

tells serve to listen for HTTP requests on port 4321.

4321 is simply the port chosen for this dashboard. It is not a requirement imposed by Astro.

The command starts with exec so the temporary shell is replaced by the actual serve process. This lets runit supervise the HTTP server directly.

After saving the file, make it executable:

chmod +x $PREFIX/var/service/dashboard/run

chmod +x gives the file permission to run as a program.

Now enable the service:

sv-enable dashboard

and verify it:

sv status dashboard

A working service should show something beginning with:

run: dashboard:

Once the production files have been deployed, the dashboard will be available at:

http://SERVER_IP:4321

9. Acquire a Termux wake lock

Android normally allows the CPU to enter deeper sleep states when the phone is idle and the screen is off.

Because this phone is intentionally running server processes, Termux can request a wake lock:

termux-wake-lock

A wake lock tells Android that Termux still has ongoing work that should be allowed to continue while the display sleeps.

It does not keep the screen on.

The phone can still be locked normally and the display can turn completely off.

The Termux notification should indicate that the wake lock is held or acquired.

A wake lock helps long-running processes remain active, but it does not completely override Xiaomi’s own background-process management. The practical solution to that is documented later.

10. Start the services after a reboot

Both sshd and the dashboard are now managed by runit.

That means the boot process should not separately start another copy of sshd or serve. It only needs to restore the wake lock and initialize the service supervisor.

The intended startup flow is:

Android boots


Termux startup

     ├── acquire wake lock


termux-services / runit

     ├── sshd
     └── dashboard

Create Termux’s boot-script directory and open a new startup script:

mkdir -p ~/.termux/boot
nano ~/.termux/boot/start-services

Inside nano, add:

#!/data/data/com.termux/files/usr/bin/sh

termux-wake-lock
source /data/data/com.termux/files/usr/etc/profile.d/start-services.sh

termux-wake-lock restores the wake lock after a reboot.

The source command loads the startup script provided by termux-services. That initializes runit, which then starts any services previously enabled with sv-enable.

In this setup those are:

sshd
dashboard

Finally, make the boot script executable:

chmod +x ~/.termux/boot/start-services

After a complete phone reboot, Android requires the phone to be unlocked once before the Termux environment becomes available.

Once it has been unlocked, Termux and its services can continue running with the screen off.

Deployment

Setup is performed once. Deployment is the normal workflow used whenever the dashboard changes.

The deployment path is:

Astro source on Ubuntu

        │ build

      dist/

        │ rsync over SSH

~/lights-dashboard/ on Android


      serve

11. Build the dashboard on Ubuntu

Inside the Astro project on the Ubuntu development computer, create a production build:

pnpm build

Astro writes the resulting static site to:

dist/

This is the directory that needs to be deployed to the phone.

12. Deploy the build to the phone

Use rsync from Ubuntu to synchronize the contents of dist/ with the dashboard directory on Android:

rsync -az --delete \
  -e "ssh -p 8022" \
  dist/ \
  SERVER_IP:~/lights-dashboard/

The important options are:

-a uses archive mode, which recursively copies the directory structure and preserves normal file metadata.

-z compresses data during transfer.

--delete removes files from the phone that are no longer present in the new dist/.

That last option is useful because Astro generates hashed asset filenames. A newer build may replace:

_astro/Card.abc123.js

with:

_astro/Card.def456.js

Without --delete, old generated files would gradually accumulate on the server.

The option:

-e "ssh -p 8022"

tells rsync to perform the transfer through SSH using Termux’s port 8022.

The trailing slash in:

dist/

means the contents of dist are synchronized rather than creating a nested dist directory on the phone.

The final structure remains:

~/lights-dashboard/index.html
~/lights-dashboard/_astro/
...

There is no need to restart serve after deploying. The existing HTTP server simply reads the updated files on subsequent requests.

13. Make deployment one command

Building and synchronizing the dashboard are always performed together, so they can be wrapped in one deployment script.

In the Astro project on Ubuntu, create a scripts/ directory and a deploy.sh file, then open that file with the nano text editor:

mkdir -p scripts
nano scripts/deploy.sh

Inside nano, add:

#!/usr/bin/env bash

set -e

pnpm build

rsync \
  -az \
  --delete \
  -e "ssh -p 8022" \
  dist/ \
  SERVER_IP:~/lights-dashboard/

echo "Dashboard deployed."

The first line:

#!/usr/bin/env bash

tells the operating system to execute the script with Bash.

set -e makes the script stop as soon as one of its commands fails.

This is particularly important for deployment: if pnpm build fails, the script should stop instead of continuing and potentially replacing the working dashboard with an invalid build.

The script then runs the same production build and rsync deployment described in the previous steps.

After saving it, make the script executable:

chmod +x scripts/deploy.sh

Then add a deploy command to the existing scripts object in package.json:

{
  "scripts": {
    "deploy": "./scripts/deploy.sh"
  }
}

The project’s other existing scripts remain unchanged.

From this point onward, deploying dashboard changes requires only:

pnpm deploy

That performs the complete workflow:

pnpm deploy


Astro production build


dist/


rsync over SSH


Android server updated

Normal operation

Once everything is configured, there are only a few commands and addresses worth remembering.

Open the dashboard:

http://SERVER_IP:4321

Connect to Termux from Ubuntu:

ssh -p 8022 SERVER_IP

Deploy dashboard changes from Ubuntu:

pnpm deploy

Check the dashboard server:

sv status dashboard

Check the SSH server:

sv status sshd

Restart the dashboard server if necessary:

sv restart dashboard

A restart is not normally required after deploying new static files. It is only useful if the serve process itself needs to be restarted.

Keeping Termux alive

On this particular Xiaomi phone, Termux is eventually killed if it is left as a background application, even with:

Battery usage: No restrictions
Background autostart: enabled
Wake lock: acquired

The reliable operating procedure is simple:

Leave Termux as the currently displayed app before locking the phone.

The screen can then turn off normally, the phone can remain unplugged, and the server continues running.

If the phone is temporarily used for something else:

finish using other app


return to Termux


leave terminal displayed


lock phone

If Android kills Termux itself, runit cannot restart the services because the supervisor is part of the Termux environment that was killed.

After a complete reboot, unlock the phone once, make sure Termux is running, leave it displayed, and then lock the phone normally.

Final architecture

The completed setup has a clear separation of responsibilities:

WLED controllers
ESP8266 + WS2812B

        │ WLED HTTP API

Browser dashboard

        │ HTTP :4321

Android server
Termux
├── runit
│   ├── sshd
│   └── dashboard / serve

└── ~/lights-dashboard/

        │ rsync over SSH :8022

Ubuntu workstation
Astro source
└── pnpm deploy

The Android phone is infrastructure.

Ubuntu remains the development environment.

The WLED controllers remain independent.

The dashboard provides a single control layer over the individual lighting installations.