CLI for mapping running local web servers to .localhost names.
  • Go 68%
  • HTML 30.1%
  • Shell 1.8%
Find a file
dikkadev 09199e139f docs(website): redesign landing page around service-based local-router workflow
This refreshes the marketing page around the tool's actual workflow: a long-running router service, per-project registration, dashboard visibility, and explicit scope boundaries. It also adds a simple nginx container for serving the site and updates the example runner to record owner metadata and refresh routes idempotently.
2026-07-14 15:05:30 +02:00
.dev feat(website): add datasheet-style landing page for local-router 2026-07-09 11:09:43 +02:00
docs feat(router): persist route state across graceful restarts with shielded restores 2026-07-09 23:09:33 +02:00
internal feat(router): persist route state across graceful restarts with shielded restores 2026-07-09 23:09:33 +02:00
packaging/systemd feat(router): persist route state across graceful restarts with shielded restores 2026-07-09 23:09:33 +02:00
skills/local-router feat(router): persist route state across graceful restarts with shielded restores 2026-07-09 23:09:33 +02:00
website docs(website): redesign landing page around service-based local-router workflow 2026-07-14 15:05:30 +02:00
.gitignore fix(router): preserve forwarded proto and improve mobile dashboard layout 2026-07-07 11:33:39 +02:00
go.mod docs: document go install workflow and adopt forge module path 2026-05-30 09:33:41 +02:00
go.sum feat: implement initial local-router service and CLI for v1 routing workflows 2026-05-29 21:59:07 +02:00
main.go feat(router): persist route state across graceful restarts with shielded restores 2026-07-09 23:09:33 +02:00
package.json feat: add Pi skill packaging and systemd service docs for local-router 2026-05-30 08:50:08 +02:00
README.md feat(router): persist route state across graceful restarts with shielded restores 2026-07-09 23:09:33 +02:00

Local Router

A Go tool for giving temporary local web servers stable, friendly browser URLs such as http://demo.localhost, while the actual target server remains separately owned by the tool or shell that started it.

See docs/spec.md for the original v1 specification.

Quick start

Install the CLI directly from the Git source:

go install forge.dikka.dev/lab/local-router@latest

Make sure Go's install directory is on your PATH:

export PATH="$(go env GOPATH)/bin:$PATH"

Allow the installed CLI to bind local port 80 without running the router as root:

sudo setcap 'cap_net_bind_service=+ep' "$(go env GOPATH)/bin/local-router"

There are two runtime parts:

  1. Start the router service — the long-running reverse proxy on 127.0.0.1:80.
  2. Register routes — tell the router which already-running local server/port should answer for a .localhost name.

Start the router service:

# Terminal 1: start the router service
local-router serve

# Or use structured JSON logs
local-router serve --json

To also expose the same routes through a tailnet/domain wildcard, point DNS at the machine and pass the base host. For example, if ppc.dikka.dev and *.ppc.dikka.dev resolve to this machine's Tailscale IP:

local-router serve --addr 0.0.0.0:80 --external-host ppc.dikka.dev

Then the dashboard is also available at http://ppc.dikka.dev, and a route named demo is also available at http://demo.ppc.dikka.dev.

Keep that process running. Then start any local web server separately. For example:

# Terminal 2: example target server
python3 -m http.server 5173 --bind 127.0.0.1

Register that target port with the router:

# Terminal 3: register a friendly URL for the target server
local-router register demo --port 5173 --title "Demo app"

The CLI talks to http://127.0.0.1 by default and sends Host: dev.localhost internally. That matters because some command-line tools do not resolve dev.localhost even when browsers handle .localhost correctly.

The register command prints the URL:

http://demo.localhost

Open that URL in the browser. The dashboard is available at:

http://dev.localhost
http://router.localhost

If the service was started with --external-host ppc.dikka.dev, the dashboard is also available at:

http://ppc.dikka.dev

and registered routes also get external URLs such as:

http://demo.ppc.dikka.dev

Help

The CLI has built-in help:

local-router --help
local-router serve --help
local-router register --help

Agent skill installation

This repo is also a Pi package. It exposes the local-router skill from skills/local-router/SKILL.md, so Pi agents can learn the CLI workflow from the same repo as the tool.

For Pi, install the package from the remote repo after pushing a commit or tag:

pi install ssh://git@git.dikka.dev:2222/lab/local-router.git@<ref>

For local Pi skill development before pushing:

pi install /home/dikka/projs/local-router

The open skills CLI can also install the bundled skill for Pi or other supported agents:

# From a local checkout of this repository
npx skills add . --skill local-router -a pi -g -y

# Or from a reachable git clone URL
npx skills add ssh://git@git.dikka.dev:2222/lab/local-router.git --skill local-router -a pi -g -y

Omit -g to install into the current project instead of the global agent skill directory. For non-Pi agents, replace -a pi with the agent name supported by npx skills.

Other agent harnesses that understand SKILL.md files can install or copy the skill from:

skills/local-router/SKILL.md

Service setup

For persistent Linux/WSL systemd setup, see docs/systemd.md. The packaged unit template runs the router as your normal Linux user, grants only the low-port bind capability needed for 127.0.0.1:80, and restores/saves route definitions across graceful service restarts.

Restored non-pinned routes are shielded from heartbeat removal until their target responds successfully once. This preserves friendly names while dev servers come back after startup without turning those routes into permanent pins.

Installing from a checkout

If you already cloned the repository and want to install that exact checkout instead of @latest, run this from the repo root:

go install .

Port 80 note

The service intentionally binds 127.0.0.1:80 so route URLs do not need :port.

If startup fails, either another process already owns port 80 or your OS requires elevated permission for low ports. The quick start uses setcap to grant only the bind capability to the installed CLI:

sudo setcap 'cap_net_bind_service=+ep' "$(go env GOPATH)/bin/local-router"
local-router serve

Core idea

  • Run one small persistent router/reverse proxy on port 80; loopback-only by default.
  • Let ad hoc local servers register and unregister routes dynamically.
  • Use .localhost hostnames by default to avoid hosts-file edits.
  • Optionally serve a configured external host and wildcard subdomain routes for tailnet/domain access.
  • Provide a live dashboard at http://dev.localhost and http://router.localhost.

CLI reference

local-router status
local-router register demo --port 5173 --title "Demo app"
local-router register demo --port 5173 --title "Demo app" --pinned
local-router register demo --port 3000 --force
local-router routes
local-router export routes.jsonl
local-router import routes.jsonl --mode set --shielded
local-router pin demo
local-router unpin demo
local-router unregister demo

Successful register prints only the final URL, for example:

http://demo.localhost

Important: local-router does not start your target web server. Start your app/dev server yourself first, then register its port.

Status

Initial v1 implementation exists: in-memory route registry with optional graceful restart snapshots, JSONL route import/export, one-time shields for restored routes, REST API, control page, reverse proxy, heartbeat cleanup, CLI commands, and a Linux/WSL systemd unit template.