Self-hosted micro-PaaS for Git-based deployments on low-resource devices.
Overview • Architecture • Prerequisites • Installation • Configuration • Deployment Flows • API Reference • Troubleshooting
MiniShinobi is a lightweight, self-hosted deployment platform designed to run on resource-constrained devices like phones running Termux, as well as any standard Linux server. It gives you a personal PaaS (Platform as a Service) — push code to GitHub, and MiniShinobi clones it, builds it, runs it, and exposes it on the internet via a Cloudflare Tunnel.
What it does:
- Provides a React dashboard to create projects, trigger deployments, and stream live logs
- Accepts GitHub-style webhook events to auto-deploy on every push
- Clones your repository, detects the framework, runs the build, and starts the process
- Manages isolated child processes for each app with automatic port assignment
- Generates per-app Nginx virtual host configs and reloads Nginx automatically
- Exposes every deployed app at
<project-name>.<your-domain>via Cloudflare Tunnel - Persists deployment history and logs in SQLite (via
sql.js— no native binaries)
Design goals:
- Zero native compilation: uses
sql.js(WebAssembly SQLite),session-file-store, and Node.js built-inchild_process - Works on 4 GB RAM · ARM devices without any native
node-gypdependencies - One backend process manages all deployed apps via in-process child spawning
Internet
└──> Cloudflare (DNS + proxy)
└──> cloudflared tunnel (authenticated tunnel to your device)
└──> Nginx on port 80 (host-based virtual routing)
├──> dashboard.<domain> ──> MiniShinobi backend (port 3000)
│ ├── serves built React frontend
│ ├── REST API (/api/...)
│ └── POST /deploy (webhook)
└──> <app>.<domain> ──> spawned app process (port 5000–5999)
| Component | Description |
|---|---|
Backend (backend/src/app.js) |
Single Express 5 server. Initialises SQLite, registers routes, serves the React build |
Deployer (src/deployer.js) |
In-process deployment queue. Serialises builds so only one runs at a time |
| Deployment Controller | Orchestrates: git → build → port → process → nginx → registry |
| Process Manager | Maintains an in-memory Map of running child_process handles per project |
| Runtime Registry | /runtime/projects.json — JSON file tracking every app's status, port, PID, etc. |
| Nginx Manager | Writes per-project .conf files to nginx/sites-enabled/ and reloads Nginx |
| SQLite (sql.js) | Stores users, projects, deployments, and streaming logs. Saved to disk every 5 s |
| Requirement | Version | Purpose |
|---|---|---|
| Node.js | 18 LTS or newer | Backend + build tooling |
| npm | bundled with Node | Package installation |
| Git | any recent | Cloning and pulling app repos |
| Nginx | any recent | Reverse proxy / virtual host routing |
| cloudflared | any recent | Cloudflare Tunnel (internet exposure) |
| PM2 (recommended) | any | Process supervision and auto-restart |
Open Termux and install everything with:
pkg update && pkg upgrade -y
pkg install nodejs-lts git nginx cloudflared -y
npm install -g pm2Note: On Termux, all binary paths live under
/data/data/com.termux/files/usr/bin/. The includedecosystem.config.jsalready uses these paths.
# Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs git nginx
# cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb
# PM2
npm install -g pm2git clone https://github.com/Mic-360/MiniShinobi.git
cd MiniShinobi# Backend
cd backend && npm install && cd ..
# Frontend
cd frontend && npm install && cd ..mkdir -p apps runtime/logs nginx/sites-enabled logs| Directory | Purpose |
|---|---|
apps/ |
Git clones of your deployed projects land here |
runtime/ |
projects.json runtime registry and per-app process logs |
runtime/logs/ |
Individual <project>.log files for running app stdout/stderr |
nginx/sites-enabled/ |
Auto-generated per-project Nginx vhost configs |
logs/ |
PM2 backend/nginx/tunnel log output |
cp backend/.env.example backend/.envOpen backend/.env and fill in all values. See the Configuration section for what each variable does.
The backend serves the React frontend as static files from frontend/dist. You must build it before starting:
cd frontend
npm run build
cd ..The frontend/dist folder is served automatically by the backend. You do not need a separate web server for the dashboard.
MiniShinobi generates per-project Nginx vhost files in nginx/sites-enabled/. Your main Nginx config just needs to include them.
Edit nginx/nginx.conf (or your system nginx.conf) and make sure it contains an include directive like:
# Termux path — adjust for your setup
include /data/data/com.termux/files/home/MiniShinobi/nginx/sites-enabled/*.conf;For standard Linux, this might be:
include /home/<user>/MiniShinobi/nginx/sites-enabled/*.conf;Verify the config and start Nginx:
nginx -t # test config
nginx -c /path/to/nginx/nginx.conf # start with custom config
# or reload if already running:
nginx -s reloadIf you haven't already, log in and create a tunnel:
cloudflared tunnel login
cloudflared tunnel create minishinobi-dashboardThis is one-time setup. MiniShinobi reuses a single tunnel and does not create a new Cloudflare Tunnel per deployment.
Copy the credentials file UUID printed by the command. You'll need it in the next step.
Edit cloudflared/config.yml (or ~/.cloudflared/config.yml):
tunnel: <your-tunnel-uuid>
credentials-file: /path/to/.cloudflared/<uuid>.json
ingress:
- hostname: dashboard.yourdomain.com
service: http://localhost:80
- hostname: '*.yourdomain.com'
service: http://localhost:80
- service: http_status:404Both the dashboard and all <app>.yourdomain.com subdomains route through Nginx on port 80. Nginx then proxies to the correct backend port.
Add DNS records in Cloudflare for dashboard.yourdomain.com and *.yourdomain.com, pointing them to the tunnel (Cloudflare will create these automatically if you use cloudflared tunnel route dns).
The included ecosystem.config.js starts the backend, Nginx, and the Cloudflare Tunnel as three supervised processes.
Termux users: The file uses Termux-specific binary and path locations. Review and update paths if you are on standard Linux.
pm2 start ecosystem.config.js
pm2 status
pm2 save # persist across reboots
pm2 startup # (Linux) generate startup scriptExpected output:
┌─────────────────────────┬─────────┬──────┬───────┐
│ name │ status │ cpu │ mem │
├─────────────────────────┼─────────┼──────┼───────┤
│ minishinobi-backend │ online │ 0% │ ~60mb │
│ minishinobi-nginx │ online │ 0% │ ~5mb │
│ minishinobi-tunnel │ online │ 0% │ ~30mb │
└─────────────────────────┴─────────┴──────┴───────┘
Open https://dashboard.yourdomain.com in your browser. You should see the MiniShinobi login page.
All backend configuration is done via backend/.env. The server will not start without the required variables.
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3000 |
Port the backend process listens on (binds to 127.0.0.1 only) |
NODE_ENV |
No | — | Set to production for secure cookies and production behaviour |
SESSION_SECRET |
Yes | — | Random string used to sign session cookies. Use a long random value |
DASHBOARD_URL |
Yes | — | Full URL of your dashboard, e.g. https://dashboard.yourdomain.com. Used by CORS and OAuth redirect |
GITHUB_CLIENT_ID |
Yes | — | OAuth App client ID from GitHub |
GITHUB_CLIENT_SECRET |
Yes | — | OAuth App client secret from GitHub |
GITHUB_CALLBACK_URL |
Yes | — | Must exactly match the callback URL in your GitHub OAuth App, e.g. https://dashboard.yourdomain.com/auth/github/callback |
DB_PATH |
Yes | — | Absolute path to the SQLite file, e.g. /home/user/MiniShinobi/runtime/db.sqlite. The directory is created automatically. Sessions are stored in a sessions/ folder next to this file |
LOGS_DIR |
No | — | Path for platform-level logs (used for reference; PM2 manages log files directly via ecosystem.config.js) |
APPS_DIR |
No | <project-root>/apps |
Where cloned repositories are stored. Each project gets a subdirectory named after its slug |
RUNTIME_DIR |
No | <project-root>/runtime |
Root of the runtime metadata directory. Contains projects.json and logs/ |
BASE_DOMAIN |
No | minishinobi.dev |
Subdomain suffix for deployed apps. If set to yourdomain.com, a project named blog becomes blog.yourdomain.com |
NGINX_SITES_ENABLED_DIR |
No | <project-root>/nginx/sites-enabled |
Where generated per-project Nginx .conf files are written |
NGINX_LISTEN_PORT |
No | 4000 |
Port used by generated app vhost server blocks. Must match the local Nginx listener your tunnel points to (Termux default in this repo is 4000) |
NGINX_RELOAD_CMD |
No | nginx -s reload |
Command to reload Nginx after a vhost config changes |
WEBHOOK_SECRET |
Yes | — | HMAC-SHA256 secret shared with GitHub (or any webhook source). Used to verify POST /deploy requests |
APP_PORT_START |
No | 5000 |
Start of the port range for deployed app processes |
APP_PORT_END |
No | 5999 |
End of the port range. MiniShinobi scans this range and picks the first free port |
BUILD_COMMAND_TIMEOUT_MS |
No | 1800000 |
Hard timeout (milliseconds) for each build command (npm install, npm run build, etc). Prevents infinite hangs |
BUILD_IDLE_TIMEOUT_MS |
No | 600000 |
Kills a build command if it produces no stdout/stderr for this long (milliseconds) |
BUILD_HEARTBEAT_MS |
No | 20000 |
Interval (milliseconds) for "command still running" log messages during long builds |
PORT=3000
NODE_ENV=production
SESSION_SECRET=change-me-to-a-long-random-string
DASHBOARD_URL=https://dashboard.yourdomain.com
GITHUB_CLIENT_ID=Ov23liXXXXXXXXXX
GITHUB_CLIENT_SECRET=abc123...
GITHUB_CALLBACK_URL=https://dashboard.yourdomain.com/auth/github/callback
DB_PATH=/home/user/MiniShinobi/runtime/db.sqlite
BASE_DOMAIN=yourdomain.com
NGINX_SITES_ENABLED_DIR=/home/user/MiniShinobi/nginx/sites-enabled
NGINX_RELOAD_CMD=nginx -s reload
WEBHOOK_SECRET=another-long-random-string
APP_PORT_START=5000
APP_PORT_END=5999
BUILD_COMMAND_TIMEOUT_MS=1800000
BUILD_IDLE_TIMEOUT_MS=600000
BUILD_HEARTBEAT_MS=20000- Go to GitHub → Settings → Developer Settings → OAuth Apps → New OAuth App
- Set Homepage URL to
https://dashboard.yourdomain.com - Set Authorization callback URL to
https://dashboard.yourdomain.com/auth/github/callback - Copy the Client ID and generate a Client Secret
- Paste both into
backend/.env
This is the standard flow for projects you create and manage in the UI.
- Log in at
https://dashboard.yourdomain.com - Click New Project and fill in the repository URL, branch, and optional build/start commands
- Click Deploy on the project card
- Watch live deployment logs stream in real time
Internally:
POST /api/deployments/project/:projectId/deploycreates aqueueddeployment record- The deployment enters the global serial queue in
deployer.js deploymentController.executeDeploymentruns: git → build → process → nginx- Status updates flow to the frontend via SSE (
GET /api/deployments/:id/logs)
Use this to auto-deploy when you push to GitHub.
Set up the webhook in GitHub:
- Go to your repository → Settings → Webhooks → Add webhook
- Set Payload URL to
https://dashboard.yourdomain.com/deploy - Set Content type to
application/json - Set Secret to the same value as your
WEBHOOK_SECRETenv variable - Choose Just the push event (or specific events)
- Save
How it works:
GitHub sends a POST /deploy request with an x-hub-signature-256 header. MiniShinobi verifies the HMAC signature against WEBHOOK_SECRET using constant-time comparison. If valid, it resolves the project from the repository URL and queues a deployment.
If the project doesn't exist yet in the database, MiniShinobi creates it automatically using the repository name as the slug.
Manual webhook request (for testing):
PAYLOAD='{"repository":{"clone_url":"https://github.com/your-org/your-repo.git"},"ref":"refs/heads/main"}'
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "your-webhook-secret" | awk '{print "sha256="$2}')
curl -X POST https://dashboard.yourdomain.com/deploy \
-H "Content-Type: application/json" \
-H "x-hub-signature-256: $SIG" \
-d "$PAYLOAD"Expected response:
{ "deploymentId": 42, "status": "queued" }The webhook also accepts
x-minishinobi-signatureas an alternative header name.
Every deployment, regardless of trigger, runs through the same pipeline:
1. Mark deployment as "building" in DB
2. git clone (first deploy) OR git fetch + checkout + pull (redeploy)
3. Read commit SHA and message
4. Read .minishinobi.json if it exists
5. Detect framework (or use project's saved commands)
6. Resolve final build command and start command
7. Run build command in a shell (stdout/stderr streamed to SSE and saved to DB)
8. Allocate port (reuse existing port if project was previously deployed)
9. Stop old process (SIGTERM → 10s → SIGKILL) and start new one
10. Write nginx vhost config; reload nginx only if config changed
11. Update runtime registry (runtime/projects.json)
12. Mark deployment as "ready" in DB with tunnel_url
13. Emit [END] to SSE clients
If any step fails, the deployment is marked as failed with the error message, and [END] is still emitted.
When a project has no custom commands set (or .minishinobi.json is absent), MiniShinobi auto-detects frameworks by checking config files first (including TypeScript variants), then package.json dependencies.
nextnuxtremixastrosveltekitvitenodestatic
| Framework | Config files checked |
|---|---|
next |
next.config.js, next.config.mjs, next.config.cjs, next.config.ts, next.config.mts, next.config.cts |
nuxt |
nuxt.config.js, nuxt.config.mjs, nuxt.config.cjs, nuxt.config.ts, nuxt.config.mts, nuxt.config.cts |
remix |
remix.config.js, remix.config.mjs, remix.config.cjs, remix.config.ts, remix.config.mts, remix.config.cts |
astro |
astro.config.js, astro.config.mjs, astro.config.cjs, astro.config.ts, astro.config.mts, astro.config.cts |
sveltekit |
svelte.config.js, svelte.config.mjs, svelte.config.cjs, svelte.config.ts, svelte.config.mts, svelte.config.cts (requires @sveltejs/kit) |
vite |
vite.config.js, vite.config.mjs, vite.config.cjs, vite.config.ts, vite.config.mts, vite.config.cts |
If no config match exists, dependency fallback is used:
nextdependency ->nextnuxtdependency ->nuxt@remix-run/nodeor@remix-run/react->remixastrodependency ->astro@sveltejs/kitdependency ->sveltekit- any
package.json->node index.htmlwithoutpackage.json->static
| Framework | Build Command | Start Command |
|---|---|---|
next |
npm install && npm run build |
npm start |
nuxt |
npm install && npm run build |
npm run start |
remix |
npm install && npm run build |
npm run start |
astro |
npm install && npm run build |
npx serve dist |
sveltekit |
npm install && npm run build |
npm run preview |
vite |
npm install && npm run build |
npx serve dist |
node |
npm install |
npm start |
static |
(none) | npx serve . |
Commands are resolved in this priority order (highest wins):
.minishinobi.jsonin the repository root — highest priority, overrides everything- Project custom commands — set via
POST /api/projectswithinstall_command,build_command,start_command, oroutput_dir - Auto-detected framework preset — fallback when nothing else is set
If you set an output_dir (for example build) but no start_command, the start command becomes npx serve <output_dir>.
Place this file in the root of your repository to override build and start commands for that specific project:
{
"build": "npm install && npm run build:prod",
"start": "node dist/server.js"
}| Field | Description |
|---|---|
build |
Shell command to install dependencies and build the project |
start |
Shell command to start the application. Receives PORT as an env variable |
Both fields are optional. Omitting build skips the build phase. Omitting start falls back to the framework preset.
The PORT environment variable is always injected into the start process — your app must listen on process.env.PORT.
All /api/* routes require authentication (GitHub OAuth session). Public routes are available for deploy and runtime control: /deploy, /apps, and /logs/:project.
| Method | Path | Description |
|---|---|---|
GET |
/auth/github |
Redirects to GitHub OAuth flow |
GET |
/auth/github/callback |
OAuth callback — redirects to dashboard on success |
GET |
/auth/me |
Returns { id, username, avatar_url } for the logged-in user. Returns 401 if not logged in |
POST |
/auth/logout |
Destroys the session. Returns { ok: true } |
All project routes are scoped to the authenticated user. Users cannot access each other's projects.
| Method | Path | Description |
|---|---|---|
GET |
/api/projects |
List all projects for the current user, ordered by creation date |
GET |
/api/projects/repos |
List repositories available from the authenticated GitHub account |
POST |
/api/projects |
Create a new project and automatically create/update push webhook |
DELETE |
/api/projects/:id |
Delete a project and all its data |
POST /api/projects body:
{
"name": "my-blog",
"repo_url": "https://github.com/you/my-blog.git",
"branch": "main",
"install_command": "npm install",
"build_command": "npm run build",
"output_dir": "dist",
"start_command": "node server.js",
"framework": "vite"
}Only name and repo_url are required. The name is slugified to generate the project subdomain. For example, "My Blog" becomes my-blog.yourdomain.com.
When creating a project, MiniShinobi also attempts to auto-create (or update) a GitHub push webhook pointing to your /deploy endpoint.
Returns 409 Conflict if a project with that slug already exists under your account.
| Method | Path | Description |
|---|---|---|
GET |
/api/deployments/project/:projectId |
List the 20 most recent deployments for a project |
POST |
/api/deployments/project/:projectId/deploy |
Trigger a new deployment. Returns 202 Accepted with { deploymentId, status: "queued" } |
GET |
/api/deployments/:id |
Get a single deployment record |
GET |
/api/deployments/:id/logs |
SSE stream — streams live log lines. Replays existing logs first, then pushes new lines in real time. Closed with a [END] event when the deployment finishes |
DELETE |
/api/deployments/:id |
Cancel a queued deployment, or stop the running deployment and its process |
SSE log event format:
{
"stream": "stdout",
"message": "Build succeeded",
"ts": "2026-03-05T12:00:00.000Z"
}stream is one of stdout, stderr, or system.
| Method | Path | Description |
|---|---|---|
POST |
/deploy |
Trigger a deployment. Supports webhook mode (repository.clone_url) and CLI mode (repo). |
Webhook mode expects HMAC signature (x-hub-signature-256 or x-minishinobi-signature).
CLI mode accepts:
{
"repo": "https://github.com/user/repo.git",
"ref": "refs/heads/main"
}If WEBHOOK_SECRET is set, include x-minishinobi-secret in CLI deploy requests.
| Method | Path | Description |
|---|---|---|
GET |
/apps |
List runtime apps from runtime/projects.json |
GET |
/logs/:project |
Stream latest deployment logs (SSE) |
POST |
/apps/:project/restart |
Restart project process |
POST |
/apps/:project/stop |
Stop project process |
DELETE |
/apps/:project |
Stop process, remove app dir, remove nginx config, reload nginx |
| Method | Path | Description |
|---|---|---|
GET |
/health |
Returns { ok: true, ts: <unix-ms> }. Use this to check if the backend is running |
runtime/projects.json is a JSON file that acts as the live state of every deployed project. It is read and written directly by the process manager and deployment controller.
Example:
{
"my-blog": {
"name": "my-blog",
"path": "/home/user/MiniShinobi/apps/my-blog",
"port": 5001,
"host": "my-blog.yourdomain.com",
"status": "running",
"pid": 14872,
"framework": "vite",
"buildCommand": "npm install && npm run build",
"startCommand": "npx serve dist",
"repoUrl": "https://github.com/you/my-blog.git",
"branch": "main",
"deploymentId": 7,
"updatedAt": "2026-03-05T12:00:00.000Z",
"createdAt": "2026-03-05T11:30:00.000Z"
}
}Possible status values:
| Status | Meaning |
|---|---|
building |
Deployment is currently running the build step |
starting |
Process was spawned, waiting for it to stabilize (1 second) |
running |
Process is live and serving traffic |
stopped |
Process was cleanly stopped (exit code 0 or SIGTERM) |
crashed |
Process exited unexpectedly with a non-zero exit code |
failed |
The deployment pipeline failed before the process started |
If runtime/projects.json becomes corrupt, a backup is saved automatically as projects.json.corrupt.<timestamp> and an empty registry is used.
MiniShinobi uses SQLite via sql.js. The database is loaded into memory at startup, and written to disk at DB_PATH every 5 seconds and on clean shutdown (SIGINT/SIGTERM).
users — GitHub OAuth user accounts
projects — deployment targets (one per repo per user)
deployments — individual deployment runs with status, commit info, port, pid
logs — streamed stdout/stderr/system lines for each deployment
See backend/db/schema.sql for the full schema.
MiniShinobi/
├── ecosystem.config.js # PM2 process definitions (backend + nginx + cloudflared)
├── backend/
│ ├── package.json
│ ├── .env # your environment variables (not committed)
│ ├── db/
│ │ └── schema.sql # SQLite schema (users, projects, deployments, logs)
│ └── src/
│ ├── app.js # Express app bootstrap, session, passport, routing
│ ├── deployer.js # Deployment queue, SSE broadcast hub
│ ├── db.js # sql.js database init and disk-save logic
│ ├── dbHelpers.js # Synchronous prepare/get/all/run helpers over sql.js
│ ├── portManager.js # Port range scanner (APP_PORT_START – APP_PORT_END)
│ ├── controllers/
│ │ └── deploymentController.js # Full deployment pipeline orchestration
│ ├── routes/
│ │ ├── auth.js # GET /auth/github, /auth/me, POST /auth/logout
│ │ ├── deploy.js # POST /deploy (webhook with HMAC verification)
│ │ ├── deployments.js # CRUD + SSE log streaming for deployments
│ │ └── projects.js # CRUD for projects
│ └── services/
│ ├── buildRunner.js # Shell command runner with buffered line output
│ ├── frameworkDetector.js # Detects Next/Vite/Node/Static by file presence
│ ├── gitManager.js # git clone / fetch / pull / commit info
│ ├── nginxManager.js # Generates vhost .conf files and reloads nginx
│ ├── processManager.js # Spawns, restarts, and stops app child processes
│ └── runtimeRegistry.js # Reads/writes runtime/projects.json
├── frontend/
│ ├── package.json
│ ├── vite.config.js
│ ├── index.html
│ └── src/
│ ├── api.js # Axios instance configured to hit the backend
│ ├── App.jsx # Root component with routing
│ ├── context/
│ │ └── AuthContext.jsx
│ ├── pages/
│ │ ├── Login.jsx
│ │ ├── Dashboard.jsx # Project list and deploy buttons
│ │ ├── Project.jsx # Project detail and deployment history
│ │ └── Deployment.jsx # Live SSE log viewer
│ └── components/
│ ├── Layout.jsx
│ └── ui/ # Button, Input, Badge, Modal
├── nginx/
│ ├── nginx.conf # Main nginx configuration
│ └── sites-enabled/ # Auto-generated per-project vhost configs (gitignored)
├── cloudflared/
│ └── config.yml # cloudflared tunnel configuration
├── apps/ # Cloned project repositories (gitignored)
├── runtime/ # projects.json + per-app logs (gitignored)
└── logs/ # PM2 process output logs (gitignored)
| Step 1: Login | Step 2: Dashboard |
|---|---|
![]() |
![]() |
| Authentication via GitHub OAuth | Overview of all your projects |
| Step 3: Create Project | Step 4: Configure |
|---|---|
![]() |
![]() |
| Add a new repository | Setup build and install commands |
| Step 5: Build Logs | Step 6: Ready |
|---|---|
![]() |
![]() |
| Real-time deployment monitoring | Successful deployment status |
| Step 7: Live App |
|---|
![]() |
| Your project live on your domain |
- Check that all required env variables in
backend/.envare set - Ensure
DB_PATHdirectory is writable - Run
node backend/src/app.jsdirectly to see the full error output - Check
logs/backend-error.logif running under PM2
- Confirm you ran
npm run buildin thefrontend/directory - The
frontend/dist/folder must exist — the backend serves it as static files - Verify
DASHBOARD_URLin.envmatches exactly what you're accessing in the browser (no trailing slash)
- Verify
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETare correct - Verify
GITHUB_CALLBACK_URLin.envexactly matches the callback URL set in the GitHub OAuth App (includinghttps://) - Check that
SESSION_SECRETis set — sessions will not work without it
- Confirm
WEBHOOK_SECRETin.envmatches the secret set in the GitHub webhook settings - Ensure
Content typeis set toapplication/jsonin the GitHub webhook config - GitHub sends the signature over the raw request body — do not modify or re-encode the payload
- Check
runtime/projects.json— confirm the project'sstatusisrunningand note theport - Test the app locally:
curl http://127.0.0.1:<port> - Check
nginx/sites-enabled/<project>.conf— confirm it was generated with the correct port - Run
nginx -tto validate the Nginx config, thennginx -s reload - Check the Cloudflare Tunnel dashboard to ensure the wildcard DNS record exists for
*.<yourdomain.com> - Check
runtime/logs/<project>.logfor app-level errors
- Open the deployment in the dashboard and read the SSE log stream
- Common causes: missing dependencies, incorrect build command, or not enough memory for the build
- Override commands using
.minishinobi.jsonin your repo root - Verify the project's
branchis correct
- MiniShinobi now logs heartbeat lines during long commands and auto-terminates stuck commands.
- Tune these in
backend/.env:BUILD_COMMAND_TIMEOUT_MS,BUILD_IDLE_TIMEOUT_MS,BUILD_HEARTBEAT_MS. - For low-end devices, set a larger timeout (example:
BUILD_COMMAND_TIMEOUT_MS=3600000). - If it keeps stalling, run manually inside the app folder:
cd apps/<project> && npm install --no-audit --no-fund && npm run build - If manual install is also slow, the issue is host network/registry access, not the deployment queue.
- Check
runtime/logs/<project>.logfor the crash reason - Ensure your app reads
PORTfromprocess.env.PORT— MiniShinobi injects this automatically - Verify your start command is correct (you can set it in
.minishinobi.json)
- Increase the range with
APP_PORT_STARTandAPP_PORT_ENDin.env - Default range is
5000–5999(1000 slots)
MiniShinobi sends SIGTERM and waits 10 seconds before sending SIGKILL. If a process is stuck, it will be force-killed after 10 seconds automatically.
This project is open source under the MIT License.
Built with ❤️ by bhaumic
on a Snapdragon 660 · 4 GB RAM · PixelExperience Android 13
Zero native compilation: sql.js + session-file-store + child_process (built-in)







