Project
←Back to projects
march 4, 2026
MiniShinobi
MiniShinobi
A self-contained deployment platform that runs inside Termux on a rooted Android device. Connect GitHub repositories, trigger deployments from a React dashboard, and every project goes live on a public subdomain via Cloudflare Tunnels — from a Snapdragon 660 with 4GB of RAM, zero monthly hosting cost, and no router configuration.
Overview
Cloud hosting is convenient until the free tier runs out. A traditional VPS costs money and requires maintenance. A Raspberry Pi is purpose-built but still needs a static IP or a home router with port forwarding. MiniShinobi is none of those things — it's a rooted Android phone in a drawer, already running Linux under Termux, already connected to the internet. The question was whether it could be production-grade.
The answer required solving three problems: no native C++ compilation on ARM Termux, no public IP or open ports on a residential connection, and no OOM kills when multiple builds trigger simultaneously on 4GB of RAM.
Architecture
MiniShinobi is a four-process system managed by PM2, with Nginx as the single public ingress point.
Internet
│
▼
Cloudflare (yourdomain.com)
│ DNS → Cloudflare Tunnel
▼
cloudflared daemon (Termux, outbound only)
│
├── dashboard.yourdomain.com → Nginx :4000 → static React build
└── *.yourdomain.com → Nginx :4000 → Express :3000 → deployed app :5000-5999Internet
│
▼
Cloudflare (yourdomain.com)
│ DNS → Cloudflare Tunnel
▼
cloudflared daemon (Termux, outbound only)
│
├── dashboard.yourdomain.com → Nginx :4000 → static React build
└── *.yourdomain.com → Nginx :4000 → Express :3000 → deployed app :5000-5999Nginx listens on port 4000 and routes traffic by hostname. The React dashboard is served as a pre-built static bundle — it never appears on a publicly reachable port. API calls from the dashboard hit Nginx, which forwards them to Express on port 3000. Deployed applications occupy ports 5000–5999, auto-assigned by portManager.js scanning for the next available port in that range.
The cloudflared daemon maintains a persistent outbound connection to Cloudflare's edge. Each deployed project gets its own named tunnel route, provisioned programmatically by the deployment engine during the deploy sequence. No inbound ports are opened. No router configuration is required.
Data Layer: sql.js Instead of better-sqlite3
The database decision is the most architecturally significant choice in the project. better-sqlite3 — the standard production SQLite binding for Node.js — requires compiling a C++ native addon during npm install. ARM Termux frequently fails this compilation step for multiple packages, making the setup brittle and non-reproducible across ARM devices.
sql.js is SQLite compiled to WebAssembly. It installs as a pure JavaScript package on any architecture, requires no native toolchain, and exposes an identical SQL query API. The trade-off is well-understood: the entire database is loaded into memory on boot, and writes must be explicitly flushed to disk. For a personal deployment server with modest data volumes — users, projects, deployments, build logs — this is acceptable. The schema is applied on every boot with CREATE TABLE IF NOT EXISTS, making the boot sequence idempotent.
The same constraint informed session storage (session-file-store over connect-sqlite3) and process spawning (child_process.spawn over node-pty), both of which are pure JavaScript alternatives to native-compilation-dependent packages.
Deployment Engine and Build Queue
The deployment engine executes a deterministic eight-step sequence when a deploy is triggered:
git clone --depth 1into an isolated build directory- Read commit SHA and message, write to the deployments table
- Run the configured install command (
npm install,pip install, etc.) - Run the configured build command (
npm run build, etc.) - Assign the next free port in the 5000–5999 range
- Serve the output: static build directories via
serve -s, Node applications vianpm startwithPORTinjected as an environment variable - Provision a Cloudflare named tunnel route and update
config.yml - Update the deployment record to
status: readywith the tunnel URL
The API returns 202 Accepted immediately with the deploymentId. The eight-step sequence runs in the background. All stdout/stderr from every step is line-buffered into the logs table in real time and simultaneously pushed to connected SSE clients.
The build queue serializes all deployments. npm install followed by npm run build for a React application can push memory usage close to the device's limit — triggering multiple builds concurrently risks OOM kills and process termination. The queue ensures one build runs at a time; subsequent deploys wait. This is not a performance limitation — it is the correct behavior for resource-constrained hardware.
Key Features
- GitHub OAuth authentication — Passport.js handles the OAuth flow; sessions persist as JSON files via
session-file-store - Real-time build logs via SSE —
GET /api/deployments/:id/logsis a Server-Sent Events stream; logs replay from the database for clients that connect after the build completes - Automatic HTTPS subdomains — every deployment gets
project-slug.yourdomain.comwith Cloudflare's edge TLS, no certificate management required - Static and dynamic app support — the engine detects the presence of an output directory and switches between
serve(static) andnpm start(Node) serving modes automatically - PM2 process management —
ecosystem.config.jsdefines the multi-process system as code; PM2 handles auto-restart on crash and log rotation - Pure JavaScript dependency tree — zero native compilation; the entire stack installs cleanly on any ARM or x86 device with Node.js
Implementation Details
Server-Sent Events for Log Streaming
SSE was chosen over WebSockets for build log streaming. Log streaming is inherently one-directional — the server pushes lines, the client displays them. SSE is exactly the right primitive for this pattern: no library required on either side, native browser EventSource API, and trivial to replay from a database by querying stored log lines and writing them to the response stream before switching to live push mode. WebSockets would add bidirectional complexity without providing any benefit.
Cloudflare Tunnels as the Networking Layer
Residential and mobile connections rarely have static public IPs, and port forwarding requires router-level access that isn't always possible. Cloudflare Tunnels invert the connection model: cloudflared makes an outbound connection to Cloudflare's edge, which routes inbound requests back through that tunnel. The result is a publicly reachable URL for every deployment with no open inbound ports, no static IP requirement, and Cloudflare's DDoS protection and edge TLS as a free side effect.
Port Range Management
Deployed applications occupy ports 5000–5999. portManager.js scans this range on each deploy to find the next unoccupied port, rather than maintaining a counter that could desync if a process dies unexpectedly. This is a reliability decision: scanning is slightly more expensive than incrementing a counter, but it is accurate regardless of what happened to previously assigned processes.
Tech Stack Rationale
| Component | Technology | Rationale |
|---|---|---|
| Runtime | Node.js | Pure JavaScript ecosystem; no native compilation on ARM |
| Database | sql.js (WebAssembly SQLite) | Zero native compilation; full SQLite capability in pure JavaScript |
| Sessions | session-file-store | Pure JS; stores sessions as JSON files, no native deps |
| Auth | Passport.js + GitHub OAuth | Standard OAuth implementation; GitHub identity is natural for a deployment platform tied to GitHub repos |
| Frontend | React + Vite + Tailwind CSS | Fast HMR in development; static build output served directly by Nginx in production |
| Reverse proxy | Nginx | Single ingress point; routes dashboard and API traffic by path, serves static assets directly |
| Tunneling | Cloudflare Tunnels | Solves the public IP problem without router configuration; programmatic tunnel provisioning via CLI |
| Process manager | PM2 | ecosystem.config.js declares the multi-process system as code; auto-restart and log rotation on resource-constrained hardware |