Stable localhost addresses for all your local HTML tools — no config required to get started.
serve-local is a lightweight Python runtime that assigns a permanent URL to every project folder on your machine. Instead of hunting for which port your tool is on, you get:
http://my-tool.localhost:9999 ← always works, regardless of what port it actually runs on
Or via direct path routing:
http://localhost:9999/tool/my-tool/
No heavy dependencies. No daemon management. One Python entrypoint boots the whole thing.
| Without serve-local | With serve-local |
|---|---|
localhost:5173 one day, :5174 the next |
http://my-app.localhost:9999 — always |
| Remember which port each tool uses | Zero memory overhead |
| Manually start each server | Wake-on-access: visiting the URL starts it |
| Dead tool = broken URL | Auto-restart on crash with circuit breaker protection |
| Orphaned Node processes holding ports | Clean process tree termination on exit |
| Root-relative asset 404s in SPAs | Intelligent fallback routing via Referer and cookies |
- Stable subdomain routing —
http://<tool-id>.localhost:9999for every tool - Path routing with SPA support —
http://localhost:9999/tool/<tool-id>/seamlessly routes root-relative assets (/@vite/client,/src/App.tsx, etc.) via Referer and session cookies - Static and Node tools — serve plain HTML files or proxy to any
node server.js/npm run dev - Wake on access — visiting a tool URL starts it automatically
- Watchdog with circuit breaker — automatically restarts crashed tools, with configurable
max_restartsto prevent CPU-eating crash loops - Persistent log buffers — per-tool stdout/stderr ring buffer survives crashes and process termination, visible directly in the UI
- Process tree termination — cleanly terminates parent and all child processes (
taskkill /F /Ton Windows) so ports are never held by zombie processes - Unbuffered streaming proxy — full support for Server-Sent Events (SSE), LLM token streams, and chunked downloads with zero proxy delay or premature timeouts
- WebSocket proxy — Vite HMR and WS-based tools work out of the box
- Config hot-reload — add or edit scanned directories without restarting the server
- Responsive launcher UI — fluid, mobile-friendly control panel with split-screen preview and live SSE updates
- Env var injection — tools declare what secrets they need; the UI securely collects and provides them
- HTTPS support — self-signed certificate generation for APIs that require a secure context
- CLI —
serve-local add,remove,install - Zero required dependencies — standard library only for the core runtime; optional extras unlock TLS and encryption
Requirements: Python 3.11+. No pip install required for core features.
git clone https://github.com/AccurateTLM13/serve-local.git
cd serve-localserve-local works out of the box, but you can configure root folders to automatically scan:
cp serve-local.config.example.json serve-local.config.jsonEdit serve-local.config.json to point to where your projects live (e.g., ~/projects or C:\Users\<user>\projects).
Run with standard Python:
python server_launcher.pyOr using uv:
uv run python server_launcher.pyThe launcher starts the control server on port 9999 and automatically opens http://localhost:9999 in your default browser. Press Ctrl+C in the terminal to cleanly shut down the server and all managed tools.
Double-click start-serve-local.bat in File Explorer. This uses pythonw to run headless in the background without keeping a command prompt window open.
Creating a Windows Desktop Shortcut:
- Right-click
start-serve-local.batin the project directory. - Select Show more options > Send to > Desktop (create shortcut).
- (Optional) Right-click the shortcut on your desktop, select Properties, click Change Icon..., and choose an icon.
- Double-clicking the shortcut will start
serve-localand launch your browser.
Tip
Stopping the background launcher on Windows:
Because pythonw runs headless without a terminal window, you can stop it anytime via PowerShell:
Stop-Process -Name pythonw -ErrorAction SilentlyContinueOr open Task Manager, find pythonw.exe, and select End Task.
serve-local runs a single Python HTTP server on port 9999.
Browser → http://my-tool.localhost:9999/ (or http://localhost:9999/tool/my-tool/)
↓
serve-local control server (port 9999)
↓ (tool detected, auto-started on internal port if stopped)
internal static or node server (e.g. port 8001, ephemeral)
↓
your tool / SPA
- Subdomain routing:
http://<tool-id>.localhost:9999All modern browsers resolve*.localhostto127.0.0.1locally without DNS or/etc/hostschanges. This isolates cookies and origin storage per tool. - Path routing:
http://localhost:9999/tool/<tool-id>/Useful when subdomain wildcards are restricted. Visiting/tool/<tool-id>automatically issues a301 Moved Permanentlyredirect to/tool/<tool-id>/with a trailing slash to ensure relative URLs resolve correctly.
Single Page Applications (Vite, Next.js, React) frequently issue root-relative requests (such as /@vite/client, /src/main.tsx, or /assets/index.js). When accessed via path routing, serve-local automatically detects these un-prefixed requests using:
- The incoming
Refererheader matching/tool/<tool-id>/... - The
_serve_local_toolsession cookie set on visit
These requests are transparently routed to the active tool's upstream server, completely avoiding 404 errors on the control plane.
serve-local streams HTTP chunks directly using unbuffered socket relaying (read1). Responses like Server-Sent Events (text/event-stream), LLM chat tokens, and large file downloads stream instantaneously without buffering in memory or dropping due to idle socket timeouts.
When tools are stopped or restarted, serve-local ensures complete cleanup:
- Windows: Executes
taskkill /F /T /PID <pid>to terminate the entire process tree synchronously. This ensures child processes (such asnode.exeor Vite dev servers spawned bynpm.cmd) do not become orphaned background zombies holding onto ports. - Unix: Delivers graceful termination signals with immediate process pruning.
If an auto-restarting tool crashes on boot (e.g. missing dependencies or code error), the watchdog monitors restart frequency. If crashes exceed max_restarts (default: 5), the circuit breaker trips, halting restart attempts to prevent 100% CPU usage. Crash logs and status remain accessible in the UI.
Copy serve-local.config.example.json to serve-local.config.json (gitignored):
{
"roots": [
{
"label": "My Projects",
"path": "~/projects",
"max_depth": 2
}
],
"projects": [
{
"label": "Special App",
"path": "~/Desktop/my-app"
}
],
"exclude": ["node_modules", "dist", ".git"],
"tls": false,
"install_dir": ""
}| Field | Type | Description |
|---|---|---|
roots |
array | Directories to scan for project folders |
roots[].path |
string | Path to scan (supports ~ and environment variables) |
roots[].label |
string | Label shown in the launcher UI |
roots[].max_depth |
int | Scan depth for project folders (default: 1) |
projects |
array | Explicitly registered project folders (always included) |
exclude |
array | Directory names to skip during scanning |
tls |
bool | Enable HTTPS on port 9443 (requires cryptography) |
install_dir |
string | Where serve-local install <url> extracts tools |
A project is any folder that contains at least one .html file, or a tool-manifest.json.
The configuration file is hot-reloaded: save changes to serve-local.config.json and the launcher updates within ~2 seconds without a restart.
Drop an optional tool-manifest.json in any project folder for custom metadata, launch options, and backend configuration:
{
"manifest_version": 1,
"id": "my-tool",
"display_name": "My Tool",
"description": "One-line description.",
"version": "1.0.0",
"category": "Utilities",
"tags": ["utility"],
"pinned": false,
"entry_file": "index.html",
"default_route": "/index.html",
"server_type": "static",
"auto_start_on_access": true,
"auto_restart": true,
"max_restarts": 5,
"preferred_port": 3001,
"launch_routes": [
{ "label": "Main", "path": "/index.html" },
{ "label": "Settings", "path": "/settings.html" }
]
}{
"server_type": "node",
"command": "node",
"args": ["server.js", "--port", "{{PORT}}"],
"environment": {
"PORT": "{{PORT}}",
"PROJECT_PATH": "{{PROJECT_PATH}}"
}
}{{PORT}} is dynamically replaced with the internal port assigned by serve-local. {{PROJECT_PATH}} resolves to the folder path.
Tools can declare secrets they need. The launcher UI prompts for them before starting the tool and securely stores them in ~/.serve-local/env-store.json:
{
"env_prompt": [
{ "key": "OPENAI_API_KEY", "label": "OpenAI API Key", "secret": true },
{ "key": "BASE_URL", "label": "API Base URL", "secret": false }
]
}| Field | Type | Default | Description |
|---|---|---|---|
manifest_version |
int | — | Format version. Use 1. |
id |
string | folder name | Stable tool identity used in URLs (<id>.localhost:9999) |
display_name |
string | folder name | Human-readable name shown in the UI |
description |
string | "" |
Description displayed in the registry |
version |
string | "" |
Version string |
category |
string | "Uncategorized" |
Category filter in the UI |
tags |
array | [] |
Searchable tag list |
pinned |
bool | false |
Pin tool to the top of the list |
entry_file |
string | auto-detected | Main HTML file for static tools |
default_route |
string | auto | Route opened on launch |
server_type |
string | "static" |
"static" or "node" |
command |
string | "node" |
Executable to run for Node/custom servers |
args |
array | [] |
Command line arguments (supports {{PORT}}, {{PROJECT_PATH}}) |
environment |
object | {} |
Extra environment variables |
auto_start_on_access |
bool | true |
Start tool automatically when its URL is visited |
auto_restart |
bool | true |
Restart automatically if process exits unexpectedly |
max_restarts |
int | 5 |
Maximum watchdog restarts before tripping circuit breaker |
preferred_port |
int | dynamic | Request a specific internal port (falls back if occupied) |
launch_routes |
array | [] |
Alternate routes shown in the UI menu |
env_prompt |
array | [] |
Secret credentials to prompt before launch |
embedded |
bool | false |
Hint: tool is designed for the split-panel iframe view |
# Add a directory as a scanned root
python server_launcher.py add ~/my-projects --label "Work" --depth 2
# Add a single project explicitly
python server_launcher.py add ~/Desktop/my-app --project --label "My App"
# Remove a path from the config
python server_launcher.py remove ~/old-project
# Install a tool from a URL (expects a zip containing tool-manifest.json)
python server_launcher.py install https://example.com/my-tool.zipEnable HTTPS for tools that require secure contexts (crypto.subtle, SharedArrayBuffer, secure WebSockets):
pip install cryptographySet "tls": true in serve-local.config.json. On start, serve-local generates a self-signed certificate at ~/.serve-local/localhost.pem and binds HTTPS to port 9443.
The test suite covers full request proxying, streaming chunk delivery, process tree management, watchdog circuit breaking, config hot-reloads, and responsive layout styles:
# Using standard pytest
pip install pytest
pytest tests/ -v
# Or using uv
uv run --with pytest pytestAll 67 tests run in under 3 seconds without requiring external services.
serve-local/
├── server_launcher.py # Entrypoint (thin — wires components and runs HTTP/S)
├── serve_local/
│ ├── config.py # Config loading + mtime hot-reload watcher
│ ├── registry.py # Project discovery, caching, slug/key helpers
│ ├── process_manager.py # Process lifecycle, taskkill tree cleanup, watchdog breaker
│ ├── proxy.py # Streaming HTTP proxy + WebSocket tunnel
│ ├── api_handler.py # HTTP handler, path routing, SPA fallback routing
│ ├── sse.py # Server-Sent Events broker
│ ├── tls.py # Self-signed cert generation + HTTPS server
│ ├── env_store.py # Encrypted env var storage
│ ├── cli.py # CLI: add / remove / install
│ └── constants.py # Shared ports, paths, and constants
├── tests/
│ ├── test_config.py # Config loading and hot reload tests
│ ├── test_registry.py # Project discovery and slug tests
│ ├── test_process_manager.py # Process tree kill, circuit breaker, log persistence
│ ├── test_proxy.py # Streaming chunk forwarding, SSE, headers
│ ├── test_tool_path_routing.py # Path routing, trailing slash redirect, SPA fallback
│ └── test_ui_layout.py # Frontend CSS layout and responsiveness tests
├── index.html # Single-file control panel UI (vanilla HTML/CSS/JS)
├── serve-local.config.example.json
├── tool-manifest.example.json
├── start-serve-local.bat # Windows background launcher
├── MANIFEST.md # tool-manifest.json field reference
├── requirements.txt
└── LICENSE
See CONTRIBUTING.md.
MIT — see LICENSE.