tunr-dev/tunr

Ship the apps your agent builds. One command (or one MCP call) turns localhost into a live HTTPS URL — tunnels, deploys, scale-to-zero.

Go

3

93 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

Started as "let me see my students' projects live", ended up as a tunnel + tiny host: tunr (r/SideProject)

Quick background: I'm a teacher. I teach programming by having students build projects with AI agents; you may remember some previous posts about course projects during the summer course last semester. The recurring problem in both: the project runs on localhost, and I need to see it right now. I…

2

Sep 29, 2026

README


tunr



Ship the apps your agent builds. One command — or one MCP call — and the thing Claude Code just wrote stops living on localhost.

Release CLI: Apache 2.0 Relay: PolyForm Shield Go Version CI Stars

tunr.sh · Docs · Dashboard · Discussions · Contributing

An agent calls tunr_deploy; the build streams and the app goes live at https://standup.tunr.sh


$ tunr deploy --name sprint
  ▲ Uploading 41 files, 210 KB
  ▲ building (nixpacks auto-detect)
  🚀 Live: https://sprint.tunr.sh
     (sleeps when idle, wakes on request)

Or don't type anything — tell your agent:

"deploy this to tunr"

…and it calls tunr_deploy over MCP and hands you back the URL.


Why

Coding agents produce a lot of small software: the internal dashboard, the one-off scraper, the tool that does exactly one annoying thing for your team. Almost all of it dies on localhost, because the gap between "it works on my machine" and "my colleague can open it" is still an afternoon of Dockerfiles, DNS and IAM.

tunr closes that gap to one command. The app builds with Nixpacks (no Dockerfile), gets an HTTPS URL, sleeps when nobody's using it and wakes on the next request — so hosting a dozen barely-used internal tools costs about what hosting one does.

The 3-second tunnel tunr started as is still here, still free — and it's the safest way to demo work-in-progress to a client: read-only mode, crash protection and a feedback button, one flag each (Client demos).


Install

# macOS / Linux — Homebrew
brew install tunr-dev/tap/tunr

# macOS / Linux — install script
curl -fsSL https://raw.githubusercontent.com/tunr-dev/tunr/main/install.sh | sh

# Go
go install github.com/tunr-dev/tunr/cmd/tunr@latest

# From source
git clone https://github.com/tunr-dev/tunr.git && cd tunr && make build

A single static binary. macOS, Linux and Windows, amd64 and arm64, no runtime dependencies. Verify it with tunr doctor.

SDKs — for driving tunnels from code rather than the shell:

pip install tunr          # Python
npm install @tunr/cli     # Node.js

Cloud — deploy & host

tunr login                        # magic-link, token goes to your OS keychain
tunr deploy --name my-app         # build + run; prints the live URL
tunr apps                         # list your apps
tunr apps logs my-app --follow    # stream build + runtime logs
tunr apps delete my-app           # remove it, free the subdomain
BuildNixpacks auto-detect — Node, Python, Go, Ruby, Rust, PHP, Java. A Dockerfile is used if present.
IsolationEvery app runs in its own gVisor sandbox with CPU, memory and disk quotas.
Scale to zeroIdle → paused with its memory reclaimed (~20 MB resident), then stopped. Wake p50 is ~150 ms.
Secrets.env files are never uploaded. Pass them with --env KEY=VALUE.
Stays upClose your laptop. The app keeps serving.
tunr deploy --name sprint --port 3000 --env DATABASE_URL=postgres://…

Status: preview. Deploy works end to end and is what the MCP tools drive. Role-based sharing, per-app SQLite and tunr rollback are not built yet — see Roadmap for what's real and what isn't.


Agent-native (MCP)

The CLI and the MCP server are two faces of the same control plane. Anything you can do in a terminal, your agent can do in a tool call.

Claude Code:

claude mcp add tunr -- tunr mcp

Claude Desktop (~/.claude/claude_desktop_config.json) / Cursor (.cursor/mcp.json):

{
  "mcpServers": {
    "tunr": { "command": "tunr", "args": ["mcp"] }
  }
}

Tools

ToolWhat it does
tunr_deployBuild & host a directory on the tunr cloud, return the live URL
tunr_list_appsList deployed apps with URL and status
tunr_app_logsRead an app's build + runtime logs (for debugging a bad deploy)
tunr_delete_appDelete an app and free its subdomain
tunr_shareOpen a temporary public tunnel to a running local port
tunr_statusList active tunnels
tunr_inspectList HTTP requests captured by a tunnel
tunr_replayReplay a captured request against your local server
tunr_stopClose a tunnel

tunr_deploy and tunr_share are deliberately distinct, and the tool descriptions say so: deploy means "host this, it should outlive my laptop", share means "expose what's running on :3000 right now". Cloud tools need tunr login first.


Client demos — for freelancers and vibecoders

Showing a client something that still runs on your laptop? Three flags make the free tunnel safe to hand to someone who isn't a developer.

tunr share with --demo, --freeze and --inject-widget: a client's delete is intercepted, a crash is hidden, feedback arrives in the terminal

tunr share -p 3000 --demo --freeze --inject-widget --qr
FlagWhat the client seesWhat actually happens
--demoButtons work — "Order deleted ✓"POST/PUT/PATCH/DELETE are answered by tunr with a 200 and never reach your app. Your database stays clean.
--freezeThe page, as it was a second agoYour dev server crashed or returned a 5xx; tunr serves the last good response (X-Tunr-Freeze-Cache: HIT) while you fix it.
--inject-widgetA 💬 Feedback button on every pageTheir message, page URL and screen size land in your terminal. No code changes — tunr injects it into the HTML.
--auto-loginAlready signed in to a demo accounttunr adds your session cookie (or Bearer … header) to every request: --auto-login "session=demo-token".
--qr · --password · --ttl 1hA QR code to open it on their phone; a password; a link that expiresAccess control that takes one flag each.

Tunnels

Still free, still unlimited, still 3 seconds.

tunr share --port 3000
#  🚀 https://abc1x2y3.tunr.sh

HTTP/HTTPS with WebSocket (HMR works), plus raw TCP, UDP and end-to-end-encrypted TLS tunnels — all multiplexed over one connection. Traffic is served from a single EU relay today; more regions are planned.

Access control — password, bearer token, IP whitelist, TTL, QR
# Basic auth (user optional)
tunr share -p 8080 --password "secret"
tunr share -p 8080 --password "client:secret"

# Bearer token — Authorization: Bearer <token> or ?token=<token>
tunr share -p 3000 --auth-token "my-super-secret-key"

# IP whitelist (CIDR)
tunr share -p 3000 --allow-ip "203.0.113.0/24"
tunr share -p 3000 --allow-ip "10.0.0.0/8,172.16.0.0/12"

# Auto-expire — the tunnel closes itself
tunr share -p 3000 --ttl 1h30m

# QR code, for opening it on a phone
tunr share -p 3000 --qr
Routing & headers — path routing, regions, header rewriting, CORS, proxies
# Path routing — one public URL, several local ports
tunr share --route /=3000 --route /api=8080

# Region hint — accepted today, takes effect once more regions launch
# (tunr currently runs a single relay in the EU)
tunr share --port 3000 --region ams

# Header rewriting
tunr share -p 3000 --header-add "X-Debug: true"
tunr share -p 3000 --header-replace "Host: internal.local"
tunr share -p 3000 --header-remove "X-Powered-By"

# Forwarded headers — X-Forwarded-For (real client IP), X-Original-URL
tunr share -p 3000 --x-forwarded-for --original-url

# CORS preflight without touching your server
tunr share -p 3000 --cors-origin "https://myapp.com"

# Behind a corporate proxy
tunr share -p 3000 --proxy http://proxy:8080

# Custom domain (Pro)
tunr share -p 3000 --domain demo.client.com
TCP / UDP / TLS — databases, SSH, game servers, zero-knowledge passthrough
# TCP — raw bytes, no HTTP parsing on the relay
tunr tcp --port 5432                          # PostgreSQL
tunr tcp --port 22 --qr                       # SSH, QR for mobile
tunr tcp --port 6379 --allow-ip 10.0.0.0/8    # Redis, restricted
tunr tcp --port 3306                          # MySQL

# UDP — DNS, game servers, anything datagram
tunr udp --port 53
tunr udp --port 27015                         # game server

# TLS — end-to-end encrypted, SNI passthrough. The relay cannot read it.
tunr tls --port 8443
Daemon, service & multi-tunnel
# Background daemon
tunr start --port 3000
tunr status
tunr stop

# Several tunnels from .tunr.json
tunr up
tunr down

# Install as a system service (systemd / launchd) — starts on boot
tunr service install --port 3000
tunr service status
tunr service uninstall
Inspect & replay — the local HTTP inspector
tunr open           # dashboard at http://localhost:19842
tunr logs --follow  # stream requests in the terminal
tunr replay <id>    # re-send a captured request to your local server

Live request/response stream, headers, body, timing, one-click replay, export as a curl command. Everything stays on your machine.

Full CLI reference
CommandDescription
Cloud
tunr deploy [dir]Build & host a project; --name, --port, --env KEY=VAL
tunr appsList your cloud apps
tunr apps logs <name>Stream logs; --follow, --tail N
tunr apps delete <name>Delete an app
Tunnels
tunr share -p PORTExpose a local port over HTTPS
tunr share -p PORT -s NAMECustom subdomain (Pro)
tunr share --route /PATH=PORTMap URL paths to local ports
tunr share -p PORT --password PASSBasic authentication
tunr share -p PORT --ttl 1hAuto-close after a duration
tunr share -p PORT --demoRead-only demo mode
tunr share -p PORT --freezeServe last-good response on crash
tunr share -p PORT --inject-widgetInject the feedback widget
tunr share -p PORT --auto-login "Cookie: s=demo"Auto-inject an auth cookie
tunr share -p PORT --domain HOSTCustom domain
tunr share -p PORT --qrPrint a QR code for the URL
tunr share -p PORT --auth-token TOKENBearer token protection
tunr share -p PORT --allow-ip CIDRIP whitelist
tunr share -p PORT --header-add/-replace/-removeRewrite headers
tunr share -p PORT --x-forwarded-for --original-urlProxy headers
tunr share -p PORT --cors-origin ORIGINCORS preflight
tunr share -p PORT --proxy URLHTTP/SOCKS5 proxy
tunr share -p PORT --region amsRelay region hint (single EU relay today)
tunr share -p PORT --jsonJSON output for CI
tunr tcp -p PORT / tunr udp -p PORT / tunr tls -p PORTTCP / UDP / TLS tunnels
tunr up / tunr downStart/stop everything in .tunr.json
tunr start / tunr stop / tunr statusDaemon mode
tunr service install|status|uninstallSystem service
Everything else
tunr login / tunr logoutAuthentication
tunr open / tunr logs / tunr replay <id>Inspector
tunr mcpStart the MCP server (stdio)
tunr config init / tunr config show.tunr.json
tunr doctorDiagnose your setup
tunr update / tunr uninstall / tunr versionMaintenance

Global flags: --relay URL (point at your own relay), --verbose.


SDKs

Python — pip install tunr
from tunr import TunrClient, TunnelOptions

client = TunrClient()

tunnel = client.share(port=3000)
print(tunnel.public_url)

db_tunnel  = client.tcp(port=5432)
dns_tunnel = client.udp(port=53)
tls_tunnel = client.tls(port=8443)

opts = TunnelOptions(
    subdomain="myapp",
    password="demo123",
    allow_ips=["10.0.0.0/8"],
    freeze=True,
    inject_widget=True,
    proxy="http://proxy:8080",
    ttl="2h",
)
tunnel = client.share(port=8080, opts=opts)

requests = client.get_requests(tunnel.subdomain)
client.replay_request(tunnel.subdomain, requests[0]["id"], port=3000)

metrics = client.get_metrics()   # Prometheus text
health  = client.health_check()  # {"status": "ok"}

tunnel.close()
Node.js — npm install @tunr/cli
import { TunrClient } from '@tunr/cli'

const client = new TunrClient()

const tunnel = await client.share(3000)
console.log(tunnel.publicUrl)

const dbTunnel  = await client.tcp(5432)
const dnsTunnel = await client.udp(53)
const tlsTunnel = await client.tls(8443)

const appTunnel = await client.share(8080, {
  subdomain: 'myapp',
  password: 'demo123',
  allowIps: ['10.0.0.0/8'],
  freeze: true,
  injectWidget: true,
  proxy: 'http://proxy:8080',
  ttl: '2h',
})

tunnel.on('ready', () => console.log('Tunnel live'))
tunnel.on('error', (err) => console.error(err))
tunnel.on('exit',  () => console.log('Tunnel closed'))

const requests = await client.getRequests('myapp')
await client.replayRequest('myapp', requests[0].id, 3000)

await tunnel.close()

Configuration (.tunr.json)

tunr config init
{
  "$schema": "https://tunr.sh/schema/.tunr.schema.json",
  "port": 3000,
  "inspectorEnabled": true,
  "dashboardPort": 19842,
  "mcp": { "enabled": true }
}

Architecture

Browser ──▶ relay.tunr.sh ──┬── [WebSocket] ──▶ tunr CLI ──▶ localhost:PORT   (tunnel)
                            │
                            └── control plane ──▶ tunr-runner ──▶ gVisor sandbox   (cloud app)
                                     │
                                Postgres + route cache

Tunnels. The CLI starts a local proxy with an embedded inspector and opens one WebSocket to the relay. The relay issues a *.tunr.sh subdomain and terminates HTTPS. HTTP, WebSocket, TCP, UDP and TLS all multiplex over that single connection — a typed message discriminator routes them.

Cloud. tunr deploy uploads a tarball to the control plane, which hands it to tunr-runner. The runner builds with Nixpacks and runs the result in a gVisor sandbox with cgroup quotas. The relay routes the subdomain straight at the container, waking it if it's asleep.

Scale to zero. Apps move HOT → WARM → STOPPED on an idle timer. WARM is a cgroup memory reclaim followed by a pause, which cuts real memory cost by ~55% while keeping wake latency around 150 ms. Health-check probes are answered at the edge so a monitored app can still fall asleep.

Observability. Prometheus metrics at /metrics, K8s probes at /healthz and /readyz on the inspector port.

Self-hosting. docker-compose.yml runs the tunnel stack (relay + Caddy + Postgres); docker-compose.runner.yml adds the cloud runner. Point the CLI at it with --relay https://tunnel.yourcompany.com or TUNR_RELAY_URL. See docs/SELF_HOSTING.md.


Security

  • Auth tokens live in the OS keychain, never in a dotfile
  • All relay traffic over TLS 1.3; tunr tls is end-to-end, the relay can't read it
  • Cloud apps run under gVisor, not bare containers
  • .env files are excluded from deploy uploads by default
  • The CLI ships no telemetry, no analytics, no phone-home
  • Supply chain: go mod verify + govulncheck in CI, cosign-signed checksums

Found a vulnerability? Don't open a public issue — see SECURITY.md.


Roadmap

Honest status. "Preview" means it works but the edges are sharp; "planned" means there is no code yet.

Status
HTTP/WS, TCP, UDP, TLS tunnels✅ Stable
Demo features (freeze, demo, widget, auto-login)✅ Stable
Access control (password, token, IP, TTL)✅ Stable
Inspector + replay, Prometheus, service install✅ Stable
Python / Node SDKs✅ Stable
Self-hosted tunnel relay✅ Stable
tunr deploy + tunr apps + logs (v0.6.1+)🚧 Preview
MCP cloud tools (tunr_deploy, tunr_app_logs, …) (v0.6.1+)🚧 Preview
Scale-to-zero (sleep/wake)🚧 Preview
Self-hosted cloud runner🚧 Preview
Role-based sharing (viewer/commenter/editor, --org acme.com)📋 Planned
Per-app SQLite + snapshots📋 Planned
tunr rollback (code and data)📋 Planned
Multi-region relay (--region is accepted; one EU relay today)📋 Planned
Persistent TCP/UDP ports📋 Backlog
GUI desktop app📋 Backlog

Comparing tunr's tunnel against ngrok, Cloudflare Tunnel, LocalXpose and localtunnel: docs/compare-tunnels.md.


Contributing

Contributions are welcome — read CONTRIBUTING.md first. Good places to start are issues labelled good first issue and help wanted.

make check      # vet + lint + test + govulncheck
make pre-push   # the above, plus a build

The relay is a separate Go module and isn't covered by the Makefile:

cd relay && go build ./cmd/server && go test ./...

Community & support

Bugs & feature requestsGitHub Issues
Questions & ideasGitHub Discussions
Security reportsPrivate advisory or dev@tunr.sh
Developer / contributor contactdev@tunr.sh
Everything else (billing, partnerships, press)contact@tunr.sh

Please follow our Code of Conduct.


License

This repository is dual-licensed by directory:

PathLicence
cmd/, internal/, sdk/ — the CLI and SDKsApache-2.0OSI-approved open source. Use it, fork it, ship it commercially.
relay/ — relay, control plane, runnerPolyForm Shield 1.0.0Source-available, not open source. Read it, modify it, run your own instance — but don't use it to compete with tunr.

Running the whole stack yourself, for yourself or inside your company, is allowed under both. See NOTICE for the exact boundary.


tunr.sh · Docs · Discord · Twitter/X

Built with 💜 in Go

ai-agents
claude-code
cli
deploy
developer-tools
golang
localhost
mcp
model-context-protocol
ngrok-alternative
paas
reverse-proxy
self-hosted
tunnel
websocket

tunr-dev/tunr

Ship the apps your agent builds. One command (or one MCP call) turns localhost into a live HTTPS URL — tunnels, deploys, scale-to-zero.

Go

3

93 commits

updated Sep 28, 2026

See the code

See what people are saying

SourceMessageScoreDate

Started as "let me see my students' projects live", ended up as a tunnel + tiny host: tunr (r/SideProject)

Quick background: I'm a teacher. I teach programming by having students build projects with AI agents; you may remember some previous posts about course projects during the summer course last semester. The recurring problem in both: the project runs on localhost, and I need to see it right now. I…

2

Sep 29, 2026

README


tunr



Ship the apps your agent builds. One command — or one MCP call — and the thing Claude Code just wrote stops living on localhost.

Release CLI: Apache 2.0 Relay: PolyForm Shield Go Version CI Stars

tunr.sh · Docs · Dashboard · Discussions · Contributing

An agent calls tunr_deploy; the build streams and the app goes live at https://standup.tunr.sh


$ tunr deploy --name sprint
  ▲ Uploading 41 files, 210 KB
  ▲ building (nixpacks auto-detect)
  🚀 Live: https://sprint.tunr.sh
     (sleeps when idle, wakes on request)

Or don't type anything — tell your agent:

"deploy this to tunr"

…and it calls tunr_deploy over MCP and hands you back the URL.


Why

Coding agents produce a lot of small software: the internal dashboard, the one-off scraper, the tool that does exactly one annoying thing for your team. Almost all of it dies on localhost, because the gap between "it works on my machine" and "my colleague can open it" is still an afternoon of Dockerfiles, DNS and IAM.

tunr closes that gap to one command. The app builds with Nixpacks (no Dockerfile), gets an HTTPS URL, sleeps when nobody's using it and wakes on the next request — so hosting a dozen barely-used internal tools costs about what hosting one does.

The 3-second tunnel tunr started as is still here, still free — and it's the safest way to demo work-in-progress to a client: read-only mode, crash protection and a feedback button, one flag each (Client demos).


Install

# macOS / Linux — Homebrew
brew install tunr-dev/tap/tunr

# macOS / Linux — install script
curl -fsSL https://raw.githubusercontent.com/tunr-dev/tunr/main/install.sh | sh

# Go
go install github.com/tunr-dev/tunr/cmd/tunr@latest

# From source
git clone https://github.com/tunr-dev/tunr.git && cd tunr && make build

A single static binary. macOS, Linux and Windows, amd64 and arm64, no runtime dependencies. Verify it with tunr doctor.

SDKs — for driving tunnels from code rather than the shell:

pip install tunr          # Python
npm install @tunr/cli     # Node.js

Cloud — deploy & host

tunr login                        # magic-link, token goes to your OS keychain
tunr deploy --name my-app         # build + run; prints the live URL
tunr apps                         # list your apps
tunr apps logs my-app --follow    # stream build + runtime logs
tunr apps delete my-app           # remove it, free the subdomain
BuildNixpacks auto-detect — Node, Python, Go, Ruby, Rust, PHP, Java. A Dockerfile is used if present.
IsolationEvery app runs in its own gVisor sandbox with CPU, memory and disk quotas.
Scale to zeroIdle → paused with its memory reclaimed (~20 MB resident), then stopped. Wake p50 is ~150 ms.
Secrets.env files are never uploaded. Pass them with --env KEY=VALUE.
Stays upClose your laptop. The app keeps serving.
tunr deploy --name sprint --port 3000 --env DATABASE_URL=postgres://…

Status: preview. Deploy works end to end and is what the MCP tools drive. Role-based sharing, per-app SQLite and tunr rollback are not built yet — see Roadmap for what's real and what isn't.


Agent-native (MCP)

The CLI and the MCP server are two faces of the same control plane. Anything you can do in a terminal, your agent can do in a tool call.

Claude Code:

claude mcp add tunr -- tunr mcp

Claude Desktop (~/.claude/claude_desktop_config.json) / Cursor (.cursor/mcp.json):

{
  "mcpServers": {
    "tunr": { "command": "tunr", "args": ["mcp"] }
  }
}

Tools

ToolWhat it does
tunr_deployBuild & host a directory on the tunr cloud, return the live URL
tunr_list_appsList deployed apps with URL and status
tunr_app_logsRead an app's build + runtime logs (for debugging a bad deploy)
tunr_delete_appDelete an app and free its subdomain
tunr_shareOpen a temporary public tunnel to a running local port
tunr_statusList active tunnels
tunr_inspectList HTTP requests captured by a tunnel
tunr_replayReplay a captured request against your local server
tunr_stopClose a tunnel

tunr_deploy and tunr_share are deliberately distinct, and the tool descriptions say so: deploy means "host this, it should outlive my laptop", share means "expose what's running on :3000 right now". Cloud tools need tunr login first.


Client demos — for freelancers and vibecoders

Showing a client something that still runs on your laptop? Three flags make the free tunnel safe to hand to someone who isn't a developer.

tunr share with --demo, --freeze and --inject-widget: a client's delete is intercepted, a crash is hidden, feedback arrives in the terminal

tunr share -p 3000 --demo --freeze --inject-widget --qr
FlagWhat the client seesWhat actually happens
--demoButtons work — "Order deleted ✓"POST/PUT/PATCH/DELETE are answered by tunr with a 200 and never reach your app. Your database stays clean.
--freezeThe page, as it was a second agoYour dev server crashed or returned a 5xx; tunr serves the last good response (X-Tunr-Freeze-Cache: HIT) while you fix it.
--inject-widgetA 💬 Feedback button on every pageTheir message, page URL and screen size land in your terminal. No code changes — tunr injects it into the HTML.
--auto-loginAlready signed in to a demo accounttunr adds your session cookie (or Bearer … header) to every request: --auto-login "session=demo-token".
--qr · --password · --ttl 1hA QR code to open it on their phone; a password; a link that expiresAccess control that takes one flag each.

Tunnels

Still free, still unlimited, still 3 seconds.

tunr share --port 3000
#  🚀 https://abc1x2y3.tunr.sh

HTTP/HTTPS with WebSocket (HMR works), plus raw TCP, UDP and end-to-end-encrypted TLS tunnels — all multiplexed over one connection. Traffic is served from a single EU relay today; more regions are planned.

Access control — password, bearer token, IP whitelist, TTL, QR
# Basic auth (user optional)
tunr share -p 8080 --password "secret"
tunr share -p 8080 --password "client:secret"

# Bearer token — Authorization: Bearer <token> or ?token=<token>
tunr share -p 3000 --auth-token "my-super-secret-key"

# IP whitelist (CIDR)
tunr share -p 3000 --allow-ip "203.0.113.0/24"
tunr share -p 3000 --allow-ip "10.0.0.0/8,172.16.0.0/12"

# Auto-expire — the tunnel closes itself
tunr share -p 3000 --ttl 1h30m

# QR code, for opening it on a phone
tunr share -p 3000 --qr
Routing & headers — path routing, regions, header rewriting, CORS, proxies
# Path routing — one public URL, several local ports
tunr share --route /=3000 --route /api=8080

# Region hint — accepted today, takes effect once more regions launch
# (tunr currently runs a single relay in the EU)
tunr share --port 3000 --region ams

# Header rewriting
tunr share -p 3000 --header-add "X-Debug: true"
tunr share -p 3000 --header-replace "Host: internal.local"
tunr share -p 3000 --header-remove "X-Powered-By"

# Forwarded headers — X-Forwarded-For (real client IP), X-Original-URL
tunr share -p 3000 --x-forwarded-for --original-url

# CORS preflight without touching your server
tunr share -p 3000 --cors-origin "https://myapp.com"

# Behind a corporate proxy
tunr share -p 3000 --proxy http://proxy:8080

# Custom domain (Pro)
tunr share -p 3000 --domain demo.client.com
TCP / UDP / TLS — databases, SSH, game servers, zero-knowledge passthrough
# TCP — raw bytes, no HTTP parsing on the relay
tunr tcp --port 5432                          # PostgreSQL
tunr tcp --port 22 --qr                       # SSH, QR for mobile
tunr tcp --port 6379 --allow-ip 10.0.0.0/8    # Redis, restricted
tunr tcp --port 3306                          # MySQL

# UDP — DNS, game servers, anything datagram
tunr udp --port 53
tunr udp --port 27015                         # game server

# TLS — end-to-end encrypted, SNI passthrough. The relay cannot read it.
tunr tls --port 8443
Daemon, service & multi-tunnel
# Background daemon
tunr start --port 3000
tunr status
tunr stop

# Several tunnels from .tunr.json
tunr up
tunr down

# Install as a system service (systemd / launchd) — starts on boot
tunr service install --port 3000
tunr service status
tunr service uninstall
Inspect & replay — the local HTTP inspector
tunr open           # dashboard at http://localhost:19842
tunr logs --follow  # stream requests in the terminal
tunr replay <id>    # re-send a captured request to your local server

Live request/response stream, headers, body, timing, one-click replay, export as a curl command. Everything stays on your machine.

Full CLI reference
CommandDescription
Cloud
tunr deploy [dir]Build & host a project; --name, --port, --env KEY=VAL
tunr appsList your cloud apps
tunr apps logs <name>Stream logs; --follow, --tail N
tunr apps delete <name>Delete an app
Tunnels
tunr share -p PORTExpose a local port over HTTPS
tunr share -p PORT -s NAMECustom subdomain (Pro)
tunr share --route /PATH=PORTMap URL paths to local ports
tunr share -p PORT --password PASSBasic authentication
tunr share -p PORT --ttl 1hAuto-close after a duration
tunr share -p PORT --demoRead-only demo mode
tunr share -p PORT --freezeServe last-good response on crash
tunr share -p PORT --inject-widgetInject the feedback widget
tunr share -p PORT --auto-login "Cookie: s=demo"Auto-inject an auth cookie
tunr share -p PORT --domain HOSTCustom domain
tunr share -p PORT --qrPrint a QR code for the URL
tunr share -p PORT --auth-token TOKENBearer token protection
tunr share -p PORT --allow-ip CIDRIP whitelist
tunr share -p PORT --header-add/-replace/-removeRewrite headers
tunr share -p PORT --x-forwarded-for --original-urlProxy headers
tunr share -p PORT --cors-origin ORIGINCORS preflight
tunr share -p PORT --proxy URLHTTP/SOCKS5 proxy
tunr share -p PORT --region amsRelay region hint (single EU relay today)
tunr share -p PORT --jsonJSON output for CI
tunr tcp -p PORT / tunr udp -p PORT / tunr tls -p PORTTCP / UDP / TLS tunnels
tunr up / tunr downStart/stop everything in .tunr.json
tunr start / tunr stop / tunr statusDaemon mode
tunr service install|status|uninstallSystem service
Everything else
tunr login / tunr logoutAuthentication
tunr open / tunr logs / tunr replay <id>Inspector
tunr mcpStart the MCP server (stdio)
tunr config init / tunr config show.tunr.json
tunr doctorDiagnose your setup
tunr update / tunr uninstall / tunr versionMaintenance

Global flags: --relay URL (point at your own relay), --verbose.


SDKs

Python — pip install tunr
from tunr import TunrClient, TunnelOptions

client = TunrClient()

tunnel = client.share(port=3000)
print(tunnel.public_url)

db_tunnel  = client.tcp(port=5432)
dns_tunnel = client.udp(port=53)
tls_tunnel = client.tls(port=8443)

opts = TunnelOptions(
    subdomain="myapp",
    password="demo123",
    allow_ips=["10.0.0.0/8"],
    freeze=True,
    inject_widget=True,
    proxy="http://proxy:8080",
    ttl="2h",
)
tunnel = client.share(port=8080, opts=opts)

requests = client.get_requests(tunnel.subdomain)
client.replay_request(tunnel.subdomain, requests[0]["id"], port=3000)

metrics = client.get_metrics()   # Prometheus text
health  = client.health_check()  # {"status": "ok"}

tunnel.close()
Node.js — npm install @tunr/cli
import { TunrClient } from '@tunr/cli'

const client = new TunrClient()

const tunnel = await client.share(3000)
console.log(tunnel.publicUrl)

const dbTunnel  = await client.tcp(5432)
const dnsTunnel = await client.udp(53)
const tlsTunnel = await client.tls(8443)

const appTunnel = await client.share(8080, {
  subdomain: 'myapp',
  password: 'demo123',
  allowIps: ['10.0.0.0/8'],
  freeze: true,
  injectWidget: true,
  proxy: 'http://proxy:8080',
  ttl: '2h',
})

tunnel.on('ready', () => console.log('Tunnel live'))
tunnel.on('error', (err) => console.error(err))
tunnel.on('exit',  () => console.log('Tunnel closed'))

const requests = await client.getRequests('myapp')
await client.replayRequest('myapp', requests[0].id, 3000)

await tunnel.close()

Configuration (.tunr.json)

tunr config init
{
  "$schema": "https://tunr.sh/schema/.tunr.schema.json",
  "port": 3000,
  "inspectorEnabled": true,
  "dashboardPort": 19842,
  "mcp": { "enabled": true }
}

Architecture

Browser ──▶ relay.tunr.sh ──┬── [WebSocket] ──▶ tunr CLI ──▶ localhost:PORT   (tunnel)
                            │
                            └── control plane ──▶ tunr-runner ──▶ gVisor sandbox   (cloud app)
                                     │
                                Postgres + route cache

Tunnels. The CLI starts a local proxy with an embedded inspector and opens one WebSocket to the relay. The relay issues a *.tunr.sh subdomain and terminates HTTPS. HTTP, WebSocket, TCP, UDP and TLS all multiplex over that single connection — a typed message discriminator routes them.

Cloud. tunr deploy uploads a tarball to the control plane, which hands it to tunr-runner. The runner builds with Nixpacks and runs the result in a gVisor sandbox with cgroup quotas. The relay routes the subdomain straight at the container, waking it if it's asleep.

Scale to zero. Apps move HOT → WARM → STOPPED on an idle timer. WARM is a cgroup memory reclaim followed by a pause, which cuts real memory cost by ~55% while keeping wake latency around 150 ms. Health-check probes are answered at the edge so a monitored app can still fall asleep.

Observability. Prometheus metrics at /metrics, K8s probes at /healthz and /readyz on the inspector port.

Self-hosting. docker-compose.yml runs the tunnel stack (relay + Caddy + Postgres); docker-compose.runner.yml adds the cloud runner. Point the CLI at it with --relay https://tunnel.yourcompany.com or TUNR_RELAY_URL. See docs/SELF_HOSTING.md.


Security

  • Auth tokens live in the OS keychain, never in a dotfile
  • All relay traffic over TLS 1.3; tunr tls is end-to-end, the relay can't read it
  • Cloud apps run under gVisor, not bare containers
  • .env files are excluded from deploy uploads by default
  • The CLI ships no telemetry, no analytics, no phone-home
  • Supply chain: go mod verify + govulncheck in CI, cosign-signed checksums

Found a vulnerability? Don't open a public issue — see SECURITY.md.


Roadmap

Honest status. "Preview" means it works but the edges are sharp; "planned" means there is no code yet.

Status
HTTP/WS, TCP, UDP, TLS tunnels✅ Stable
Demo features (freeze, demo, widget, auto-login)✅ Stable
Access control (password, token, IP, TTL)✅ Stable
Inspector + replay, Prometheus, service install✅ Stable
Python / Node SDKs✅ Stable
Self-hosted tunnel relay✅ Stable
tunr deploy + tunr apps + logs (v0.6.1+)🚧 Preview
MCP cloud tools (tunr_deploy, tunr_app_logs, …) (v0.6.1+)🚧 Preview
Scale-to-zero (sleep/wake)🚧 Preview
Self-hosted cloud runner🚧 Preview
Role-based sharing (viewer/commenter/editor, --org acme.com)📋 Planned
Per-app SQLite + snapshots📋 Planned
tunr rollback (code and data)📋 Planned
Multi-region relay (--region is accepted; one EU relay today)📋 Planned
Persistent TCP/UDP ports📋 Backlog
GUI desktop app📋 Backlog

Comparing tunr's tunnel against ngrok, Cloudflare Tunnel, LocalXpose and localtunnel: docs/compare-tunnels.md.


Contributing

Contributions are welcome — read CONTRIBUTING.md first. Good places to start are issues labelled good first issue and help wanted.

make check      # vet + lint + test + govulncheck
make pre-push   # the above, plus a build

The relay is a separate Go module and isn't covered by the Makefile:

cd relay && go build ./cmd/server && go test ./...

Community & support

Bugs & feature requestsGitHub Issues
Questions & ideasGitHub Discussions
Security reportsPrivate advisory or dev@tunr.sh
Developer / contributor contactdev@tunr.sh
Everything else (billing, partnerships, press)contact@tunr.sh

Please follow our Code of Conduct.


License

This repository is dual-licensed by directory:

PathLicence
cmd/, internal/, sdk/ — the CLI and SDKsApache-2.0OSI-approved open source. Use it, fork it, ship it commercially.
relay/ — relay, control plane, runnerPolyForm Shield 1.0.0Source-available, not open source. Read it, modify it, run your own instance — but don't use it to compete with tunr.

Running the whole stack yourself, for yourself or inside your company, is allowed under both. See NOTICE for the exact boundary.


tunr.sh · Docs · Discord · Twitter/X

Built with 💜 in Go

ai-agents
claude-code
cli
deploy
developer-tools
golang
localhost
mcp
model-context-protocol
ngrok-alternative
paas
reverse-proxy
self-hosted
tunnel
websocket

Languages

Go

81.4%

Shell

6.6%

HTML

6.4%

JavaScript

2.2%

TypeScript

1.0%