|
Some checks failed
CI / lint-launcher (push) Successful in 36s
CI / test-launcher (push) Has been cancelled
CI / lint-frontend (push) Successful in 35s
CI / build-frontend (push) Successful in 34s
CI / debian-package (push) Has been skipped
CI / test (push) Successful in 40s
Deploy zsus / deploy-zsus (push) Failing after 20s
|
||
|---|---|---|
| .commits | ||
| .forgejo/workflows | ||
| .github/workflows | ||
| .sisyphus/notepads/compose-app-support | ||
| data/.launcher | ||
| debian | ||
| deploy | ||
| docs | ||
| examples | ||
| frontend | ||
| launcher | ||
| scripts | ||
| .dockerignore | ||
| .env.example | ||
| .env.tail | ||
| .env.tailscale | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .quickfix-run.log | ||
| AGENTS.md | ||
| API.md | ||
| bun.lock | ||
| CHANGELOG.md | ||
| DEPLOYMENT.md | ||
| docker-compose-tail.yml | ||
| docker-compose-tail.yml.bak | ||
| docker-compose.yml | ||
| lefthook.yml | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| self-update.sh | ||
| version.json | ||
Node Hub
A desktop-style launcher for managing and running multiple applications in isolated Docker containers. Supports Node.js apps, Docker Compose multi-service apps, Dockerfile builds, pre-built images, and Replit projects.
Table of Contents
- Overview
- Key Features
- Quick Start
- Architecture
- App Packaging Modes
- Usage
- Environment Management
- Authentication
- HTTP API
- CLI Interface
- Forgejo Integration
- Configuration
- Deployment
- Troubleshooting
- License
Overview
The Node App Launcher provides an itch.io-style interface for Node.js applications, allowing users to upload app bundles and run each one in its own Docker worker container. Each app runs in isolation using a fixed base image with the app code injected as a filesystem payload.
Key Features
- Isolated Execution: Each app runs in its own Docker container
- Immutable Base: Single base image for all apps, ensuring consistency
- Per-App Payloads: Apps are injected as read-only filesystem overlays
- Persistent Storage: Dedicated volumes for app data
- Web Interface: Dashboard for managing apps
- Reverse Proxy Access: Open apps through
/proxy/:instanceIdwithout exposing app ports - Lifecycle Controls: Stop, view logs, and delete app instances from the UI/API
- CLI Control: Command-line interface for automation
- Docker Compose Integration: Multi-service orchestration support
- Replit Project Support: Deploy Replit projects directly with Nix runtime
- Staged Deployment: Configure environment variables before starting apps
Architecture
Core Components
- Launcher Core - Main orchestration engine
- Container Manager - Handles Docker container lifecycle
- App Validator - Validates app packages and manifests
- Docker Compose Generator - Creates orchestration configurations
- Frontend UI - Management interface
- CLI Interface - Command-line control
Runtime Model
- Immutable Worker Image: Single Docker image with Node.js runtime
- Per-Run Payload: App code injected as read-only filesystem mount
- Data Volumes: Persistent storage for each app
- Resource Isolation: Each app runs in its own container namespace
- Proxy-First Access: Apps are reached through launcher proxy routes, not fixed host ports
Quick Start
Deploy Example Apps
Quickly test Node Hub with these example repositories:
Node.js Hello World:
# Deploy via Git
https://forgejo.tail018ac4.ts.net/Mark/node-hub-hello-world-example.git
Dockerfile Example:
https://forgejo.tail018ac4.ts.net/Mark/node-hub-dockerfile-example.git
Replit Project Example:
https://forgejo.tail018ac4.ts.net/Mark/node-hub-replit-example.git
App Packaging Modes
| Mode | Use When | You Provide | Launcher Does |
|---|---|---|---|
| Node App | Simple apps with one service | ZIP with package.json |
Runs in Node.js container, auto npm install |
| Compose App | Multi-service apps (web + db) | ZIP with docker-compose.yml |
Uses your compose file with security restrictions |
| Dockerfile | Custom Docker builds | ZIP with Dockerfile |
Builds image and runs container |
| Pre-built Image | Use existing Docker images | app.yml with image reference |
Pulls and runs image directly |
| Replit Project | Replit projects with .replit |
ZIP with .replit config |
Uses Nix runtime, handles dependencies |
Mode 1: Node Apps (Simple)
For simple single-service Node.js apps, just zip your code and upload. The launcher handles containerization for you.
Package as ZIP:
myapp.zip/
├── app.yml # Manifest
├── package.json # npm package file
├── index.js # Your entry point
└── ... # Other app files
app.yml manifest:
id: "my-node-app"
name: "My Node App"
version: "1.0.0"
port: 3000
Optional manifest fields:
id: "my-app"
name: "My App"
version: "1.0.0"
entry:
command: ["node", "server.js"] # Start command (default: node index.js)
cwd: "." # Working directory (default: ".")
port: 3000
env:
NODE_ENV: "production"
DATABASE_URL: "postgres://..."
persistentDataDir: ".data" # Path for persistent storage
Example apps:
- Node.js:
examples/apps/hello-world/or repohttps://forgejo.tail018ac4.ts.net/Mark/node-hub-hello-world-example.git - Compose:
examples/apps/compose-example/or repohttps://forgejo.tail018ac4.ts.net/Mark/node-hub-compose-example.git - Dockerfile:
examples/apps/dockerfile-example/or repohttps://forgejo.tail018ac4.ts.net/Mark/node-hub-dockerfile-example.git - Image:
examples/apps/image-example/or repohttps://forgejo.tail018ac4.ts.net/Mark/node-hub-image-example.git
Mode 2: Compose Apps (Multi-Service)
For complex apps with multiple services (web server + database, etc.), use the compose mode.
Package as ZIP:
mycompose.zip/
├── app.yml # Manifest with type: compose
├── docker-compose.yml # Your compose file
└── ... # Optional: other files (not env_file)
app.yml manifest:
id: "my-compose-app"
name: "My Compose App"
version: "1.0.0"
type: "compose"
port: 80
compose:
proxyService: "web" # The service to proxy via /proxy/:instanceId
docker-compose.yml:
version: "3.8"
services:
web:
image: "nginx:alpine"
depends_on:
- db
db:
image: "postgres:16"
environment:
POSTGRES_PASSWORD: "secret"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: {}
Important:
- The launcher proxies exactly one service (the
proxyService) via/proxy/:instanceId/* - Only that service publishes a port (loopback-only, random host port)
- Other services (like
db) are internal only - not exposed to the host - The payload is mounted at
/payloadin the proxy service container
Example:
- See
examples/apps/compose-example/
Mode 3: Dockerfile Apps (Custom Builds)
For apps requiring custom Docker builds.
Package as ZIP:
myapp.zip/
├── app.yml # Manifest with type: dockerfile
├── Dockerfile # Your Dockerfile
├── package.json # Optional: npm dependencies
└── ... # App files
app.yml manifest:
id: "my-dockerfile-app"
name: "My Dockerfile App"
version: "1.0.0"
type: "dockerfile"
port: 8080
env:
PORT: "8080"
Example:
- Repo:
https://forgejo.tail018ac4.ts.net/Mark/node-hub-dockerfile-example.git
Mode 4: Pre-built Image Apps (Fastest)
For using existing Docker images without building.
Package as ZIP:
myapp.zip/
└── app.yml # Only app.yml required!
app.yml manifest:
id: "my-image-app"
name: "My Image App"
version: "1.0.0"
type: "image"
image: "nginx:alpine"
port: 80
env:
NGINX_HOST: "localhost"
Example:
- Repo:
https://forgejo.tail018ac4.ts.net/Mark/node-hub-image-example.git
Compose Security Limits
For security, the launcher rejects compose files with:
- ❌ Bind mounts (
./,../, absolute paths involumes:) - ❌ Anonymous volumes (must be named volumes only)
- ❌
privileged,network_mode: host,pid: host,ipc: host,devices,cap_add,security_opt,extra_hosts - ❌
${VAR}interpolation in compose YAML - ❌
external: truenetworks or volumes - ❌ Volume/network driver options (named volumes only)
- ❌
env_file:in compose (use app.yml env or env editor instead)
Note: build: is now allowed for type: dockerfile apps.
Usage
Web Interface
- Open
http://localhost:3000 - Click "Upload New App" or "Launch from Git Repository"
- Select app type from dropdown (Auto/Node/Compose/Dockerfile/Image)
- If deploying from Git, the system scans for
.env.examplefiles automatically - Configure environment variables (see Environment Management below)
- Click deploy - the app validates and launches automatically
- Or check "Stage only" to configure environment before starting
- Use "Open App" to access the proxied app output
- Use "View Logs", "Stop", "Environment", or "Delete" to manage instances
Staged Deployment
For apps that need environment configuration before starting:
- Check "Stage only (configure env before starting)" when uploading
- App appears in "Staged Apps" section
- Click "Configure Env" to add environment variables
- Click "Start" when ready to launch
See API.md for staged deployment API endpoints.
Environment Management
Automatic Env File Scanning
When deploying from Git, Node Hub automatically scans for environment files:
.env.env.example.env.sample.env.local.env.*
Extracted environment variables are displayed in the UI before deployment, allowing you to:
- Preview required variables
- Edit values before launch
- Upload custom
.envfiles
Environment Variable Editor
Each running app has an "Environment" button in the UI:
- View current environment variables
- Edit values inline
- Add new variables
- Delete existing variables
- Changes trigger automatic container restart
- Persists across app restarts
API Endpoints:
# Get current env vars
GET /api/apps/:instanceId/env
# Update env vars (restarts container)
POST /api/apps/:instanceId/env
Content-Type: application/json
{"KEY": "value", "PORT": "3000"}
# Preview env files in Git repo
POST /api/apps/preview-env
Content-Type: application/json
{"repoUrl": "https://git.example.com/user/repo.git", "ref": "main"}
Authentication
Node Hub supports multiple authentication methods:
Session-Based (Web UI)
Users log in via the web interface with username/password. Sessions are stored in HTTP-only cookies with 24-hour expiration.
# First-time setup
curl -X POST http://localhost:8000/api/auth/setup \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "secure-password"}'
# Login
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "secure-password"}' \
-c cookies.txt
API Token (Legacy)
Set LAUNCHER_API_TOKEN for simple API access:
curl -H "Authorization: Bearer YOUR_LAUNCHER_API_TOKEN" \
http://localhost:8000/api/apps
Personal Access Tokens (PAT) - Recommended
Users can create PATs for API access without using their password:
# Create PAT (via web UI or API when logged in)
# Use PAT in API calls
curl -H "Authorization: Bearer YOUR_PAT" \
http://localhost:8000/api/apps
Authentication Flow
- First setup: Create admin user via
/api/auth/setup(one-time) - Web UI: Login with username/password, receive session cookie
- API access: Use PAT or legacy API token
- Health check:
/healthand/api/versionare public (no auth)
Security Notes
- Passwords hashed with PBKDF2 (100k iterations)
- Session cookies are HTTP-only and secure (when TLS enabled)
- PATs are hashed before storage (SHA-256)
- Rate limiting applies to all authenticated endpoints
HTTP API
# List all running instances
curl http://localhost:8000/api/apps
# Upload and launch a Node app (ZIP)
curl -X POST http://localhost:8000/api/apps \
-F "app=@myapp.zip"
# Launch from git repository
curl -X POST http://localhost:8000/api/apps/repo \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"repoUrl": "https://git.example.com/user/repo.git",
"ref": "main",
"subdir": "path/to/app"
}'
# Pull latest (update repo-launched app)
curl -X POST http://localhost:8000/api/apps/instance-id/pull \
-H "Authorization: Bearer YOUR_TOKEN"
# Get instance status
curl http://localhost:8000/api/apps/instance-id/status
# Get instance logs
curl http://localhost:8000/api/apps/instance-id/logs
# Stop instance
curl -X POST http://localhost:8000/api/apps/instance-id/stop
# Delete instance
curl -X DELETE http://localhost:8000/api/apps/instance-id
# Access proxied app
curl http://localhost:8000/proxy/instance-id/
CLI Interface
# Launch an app
node launcher/index.js launch /path/to/app.zip
# Stop a running app
node launcher/index.js stop instance-id
# Restart an app
node launcher/index.js restart instance-id
# Check app status
node launcher/index.js status instance-id
# View app logs
node launcher/index.js logs instance-id
# List all apps
node launcher/index.js list
# Delete an app
node launcher/index.js delete instance-id
# Pull latest git changes for an app
node launcher/index.js pull instance-id
# Stage an app for launch
node launcher/index.js stage /path/to/app.zip instance-id
# Start a staged app
node launcher/index.js start instance-id
# Get environment variables
node launcher/index.js env get instance-id
# Update environment variables
node launcher/index.js env set instance-id KEY=VALUE [KEY=VALUE ...]
# Deploy from git repository
node launcher/index.js deploy https://github.com/user/repo.git instance-id
Forgejo/Gitea Integration
As a Deployment Target
The launcher is a deployment target for Forgejo Actions. After your CI tests pass, Forgejo can trigger a deployment to the launcher.
This is NOT a Forgejo runner (it doesn't run your CI tests). It's a destination where deployments go after testing.
Setup
-
Configure API token in the launcher:
LAUNCHER_API_TOKEN=your-secret-token -
Add secrets in your Forgejo repository:
LAUNCHER_URL- e.g.,https://launcher.yourdomain.comLAUNCHER_TOKEN- the token from step 1
-
Add workflow to
.forgejo/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install && npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Launcher
env:
LAUNCHER_URL: ${{ secrets.LAUNCHER_URL }}
LAUNCHER_TOKEN: ${{ secrets.LAUNCHER_TOKEN }}
run: |
RESPONSE=$(curl -s -X POST "$LAUNCHER_URL/api/apps/repo" \
-H "Authorization: Bearer $LAUNCHER_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"repoUrl\": \"${{ github.server_url }}/${{ github.repository }}\",
\"ref\": \"${{ github.ref_name }}\"
}")
JOB_ID=$(echo $RESPONSE | jq -r '.jobId')
echo "Job ID: $JOB_ID"
# Poll for completion
for i in $(seq 1 60); do
sleep 2
STATUS=$(curl -s "$LAUNCHER_URL/api/jobs/$JOB_ID" | jq -r '.status')
echo "Status: $STATUS"
if [ "$STATUS" = "succeeded" ]; then
echo "Deployed!"
exit 0
elif [ "$STATUS" = "failed" ]; then
echo "Failed!"
exit 1
fi
done
echo "Timeout"
exit 1
Using Forgejo Docker Registry
You can also use Forgejo as a Docker registry for your images:
-
Build and push in your Forgejo workflow:
- name: Build and push run: | docker build -t ${{ secrets.FORGEJO_URL }}/${{ github.repository }}/myapp:latest . echo ${{ secrets.FORGEJO_TOKEN }} | docker login ${{ secrets.FORGEJO_URL }} -u ${{ github.actor }} --password-stdin docker push ${{ secrets.FORGEJO_URL }}/${{ github.repository }}/myapp:latest -
Reference in compose app:
services: web: image: "https://git.yourdomain.com/youruser/yourrepo/myapp:latest"
Configuration
Launcher Configuration
The launcher can be configured via environment variables:
# Port (default: 8000)
LAUNCHER_PORT=8000
# Directories
LAUNCHER_PAYLOAD_DIR=/path/to/payloads
LAUNCHER_COMPOSE_DIR=/path/to/compose
LAUNCHER_STATE_FILE=/path/to/state/containers.json
# Docker
DOCKER_HOST=unix:///var/run/docker.sock
LAUNCHER_BASE_IMAGE=node:20-bookworm-slim
LAUNCHER_DOCKER_TIMEOUT_MS=15000
# Security
LAUNCHER_API_TOKEN=change-me
LAUNCHER_REPO_HOST_ALLOWLIST=github.com,git.example.com
LAUNCHER_ALLOW_INSECURE_HTTP=false
LAUNCHER_GIT_TIMEOUT_MS=60000
# Job queue
LAUNCHER_JOB_CONCURRENCY=1
LAUNCHER_JOB_MAX=500
# Zip extraction limits
LAUNCHER_ZIP_MAX_ENTRIES=2000
LAUNCHER_ZIP_MAX_UNCOMPRESSED_BYTES=209715200
LAUNCHER_ZIP_MAX_FILE_BYTES=52428800
# Forgejo integration (optional)
FORGEJO_URL=https://git.example.com
FORGEJO_TOKEN=your-forgejo-api-token
TLS / SSL
For TLS/SSL configuration, see the Deployment Guide. It covers:
- Reverse proxy setup (recommended for production)
- Native TLS for simple deployments
- All TLS environment variables
- WebSocket proxy requirements
App Configuration
Each app can specify its own configuration in app.yml:
id: "my-app-id" # Unique per app (required)
name: "My App" # Display name (required)
version: "1.0.0" # App version (required)
type: "compose" # "compose" for multi-service, omit for Node (optional)
port: 3000 # Port the app listens on (required)
entry:
command: ["node", "index.js"] # Command to start (Node mode)
cwd: "." # Working directory
env:
NODE_ENV: "production" # Environment variables
persistentDataDir: ".data" # Path for persistent storage
compose:
proxyService: "web" # Service to proxy (compose mode)
Docker Integration
Container Security
- Non-root Execution: Containers run as non-root user
- Capability Dropping: Minimal capabilities required
- Read-only Payloads: App code mounted as read-only
- Isolated Networks: Each app in its own network namespace
- Proxy-Only Ports: Apps only accessible via proxy, not directly
How the Proxy Works
- Each app container publishes its port to a random host port bound to localhost only (
127.0.0.1::PORT) - The launcher proxy routes requests from
/proxy/:instanceId/*to that random port - This means:
- Apps are not directly accessible from the network
- You can run multiple apps without port conflicts
- The launcher handles routing
WebSocket Support
The proxy supports WebSocket upgrade connections. Apps can use WebSockets for real-time features:
// Client connects via the proxy
const ws = new WebSocket("ws://localhost:8000/proxy/my-app/ws");
When using WebSockets behind a reverse proxy (like Nginx or Caddy), ensure the proxy supports the Connection: Upgrade header.
Automatic Header Rewriting
The proxy automatically rewrites headers to ensure apps work correctly behind the subpath:
- Location headers: Redirects like
Location: /dashboardbecomeLocation: /proxy/:instanceId/dashboard - Set-Cookie headers: Cookies without a Path get
Path=/proxy/:instanceIdadded - Absolute URLs: Preserved unchanged (e.g.,
https://external.com/page)
This allows apps to issue redirects and set cookies without knowing they're behind a proxy.
Security Headers
The proxy strips sensitive headers before forwarding:
AuthorizationCookie- Any headers listed in the
Connectionheader (hop-by-hop headers)
Terminology
- Reverse proxy: the launcher accepts inbound requests and forwards them to a launched app.
- Port forwarding: direct host-port exposure of an app port; the launcher avoids this for app ingress by using loopback-only publishing plus
/proxy/:instanceId/*. - Forward proxy: a client-side proxy for outbound traffic; not used by this project.
Development
Project Structure
node-app-launcher/
├── launcher/ # Core launcher code
│ ├── core/ # Main orchestration
│ ├── container/ # Docker container management
│ ├── validation/ # App validation
│ ├── config/ # Configuration
│ └── test/ # Tests
├── frontend/ # Web interface
├── examples/ # Example apps
│ └── apps/
│ ├── smoke-test-app/ # Simple Node app
│ ├── hello-world/ # Another Node example
│ └── compose-example/ # Multi-service compose
├── docker-compose.yml # Development environment
└── README.md
Running Tests
cd launcher
npm test
Deployment
Production Deployment
# Build Docker images
docker-compose build
# Deploy
docker-compose up -d
# Access at http://localhost:3000
Tailscale Sidecar Deployment
For private networking with Tailscale, use docker-compose-tail.yml which runs the launcher UI and API inside the Tailscale container's network namespace:
# Copy and configure environment
cp .env.example .env
# Edit .env and set:
# - LAUNCHER_API_TOKEN
# - LAUNCHER_UI_PORT=80
# - TAILSCALE_AUTHKEY
# Deploy with Tailscale sidecar
docker-compose -f docker-compose-tail.yml up -d --build
# Verify Tailscale connectivity
docker exec nodehub-tailscale tailscale status
# Access via Tailscale MagicDNS
curl http://nodehub.tail018ac4.ts.net:${LAUNCHER_UI_PORT:-80}
Key features of Tailscale sidecar mode:
- No host port publishing required
- Services use
network_mode: "service:tailscale"to share Tailscale's network namespace - Access via Tailscale MagicDNS (e.g.,
nodehub.tail018ac4.ts.net) - UI port configurable via
LAUNCHER_UI_PORTenvironment variable - CORS origins include Tailscale domain automatically
Running the Launcher in Docker
The launcher itself can run in a container, but it needs access to Docker:
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8000:8000 \
-e LAUNCHER_API_TOKEN=secret \
-e LAUNCHER_PROXY_TARGET_HOST=host.docker.internal \
node-app-launcher
If your Docker does not support host.docker.internal, use a host-gateway mapping (Docker 20.10+):
docker run -d \
--add-host=host.docker.internal:host-gateway \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8000:8000 \
-e LAUNCHER_API_TOKEN=secret \
-e LAUNCHER_PROXY_TARGET_HOST=host.docker.internal \
node-app-launcher
Troubleshooting
Common Issues
- Docker not running: Ensure Docker daemon is started
- Permission denied: Check Docker socket permissions
- Proxy errors: Verify app is running and reachable through
/proxy/:instanceId - Invalid app package: Check app structure and manifest
Rollout Verification Checklist
# 1) Launcher API is healthy
curl -fsS http://127.0.0.1:8000/health
# 2) Run the proxy E2E smoke (requires Docker)
./scripts/proxy-e2e-smoke.sh
If the proxy smoke fails, inspect the API logs and the running app container:
tail -n +1 .launcher/api-proxy-smoke.log
docker ps --filter 'name=launcher-app-'
Debug Mode
DEBUG=launcher:* node launcher/index.js launch app.zip
View Logs
# Via API
curl http://localhost:8000/api/apps/instance-id/logs
# Or directly
docker logs launcher-app-instance-id
Recent Changes
v2.0.0 - New Features
- New App Types: Added support for Dockerfile and pre-built image deployments
- App Type Selection: Dropdown in UI to select Node/Compose/Dockerfile/Image or Auto-detect
- Environment Variable Editor: Edit env vars without redeploying (persists across restarts)
- Env File Scanning: Automatic discovery of
.env.example,.env.*files from Git repos - Env File Upload: Upload
.envfiles during deployment - Payload Mount Fix: Fixed bug where payload directory wasn't properly mounted causing "Cannot find module" errors
- Tailscale API Access: API now accessible via Tailscale MagicDNS domain
- Example Repos: Created standalone example repos on Forgejo for all app types
v1.x - Original Features
- Node.js and Docker Compose app support
- Git repository deployment
- Web UI and REST API
- Container lifecycle management
- Reverse proxy access
- Forgejo integration
License
MIT License - see LICENSE file for details.