Tunly is a lightweight, open-source HTTP tunnel solution for developers who want full control and instant setup.
No signup. No limits. No tracking. Just you, your app, and a tunnel. Keep everything on your infrastructure. Full control. No vendor lock-in.
Download Tunly · Quick Start · Report a bug
Build and install the CLI from crates.io:
cargo install tunly --lockedOr download a prebuilt archive from Releases.
Every archive includes tunly, tunly-server, and tunly-client.
# On your VPS
tunly-server --bind 0.0.0.0:8080
# On your laptop
tunly-client --remote-host your-server.com:8080 --local 127.0.0.1:3000Your localhost is now reachable through a shareable Tunly URL.
Apple Silicon and Intel Macs can use the same universal download:
brew install https://github.com/0xReLogic/Tunly/releases/latest/download/tunly.rbOr install manually:
curl -LO https://github.com/0xReLogic/Tunly/releases/latest/download/tunly-macos-universal.tar.gz
tar xzf tunly-macos-universal.tar.gz
sudo install -m 755 tunly tunly-client tunly-server /usr/local/bin/The universal binary runs natively on both Apple Silicon and Intel Macs.
Check the installation and optionally keep the client connected in the background:
tunly doctor
tunly install --remote-host tunly.allenarch.dev:8080 --local 127.0.0.1:3000 \
--token-url https://tunly.allenarch.dev/tokenRemove the background service with tunly uninstall.
Point your domain DNS at the Tunly server, expose port 443, and let Tunly
obtain and renew a Let's Encrypt certificate automatically:
tunly-server --bind 0.0.0.0:443 \
--acme-domain tunly.allenarch.dev \
--acme-email you@example.com \
--acme-cache /var/lib/tunly/acmeTunly uses TLS-ALPN validation and persists the ACME account and certificate
cache so renewals do not require a server restart. Use --acme-staging while
testing to avoid Let's Encrypt production rate limits.
- Self-hosted: run the server on your VPS and keep traffic under your control.
- Simple: connect a local app and share one URL without creating an account.
- Secure by default: ephemeral, single-use tokens are bound to the client IP and session.
- Fast: native Rust, WebSockets, HTTP/2, compression, and a small binary footprint.
- Observable: built-in dashboard with real-time tunnel monitoring, request logs, and bandwidth tracking. Structured logs and Prometheus metrics included.
- Cross-platform: prebuilt binaries and
cargo installfor Linux, macOS, and Windows.
Fastest way to deploy Tunly server + dashboard:
git clone https://github.com/0xReLogic/Tunly
cd Tunly
docker-compose upThen:
- Server: http://localhost:8080
- Dashboard: http://localhost:3000
The dashboard shows active tunnels, request logs, bandwidth usage, and system statistics. Get a token from the dashboard and start tunneling.
- Download
tunly-clientandtunly-serverfor your OS from Releases - Start the server on your VPS or cloud:
tunly-server --bind 0.0.0.0:8080- Run the client locally:
tunly-client --remote-host your-server.com:8080
- When prompted, get a token from
http://your-server.com:8080/tokenand paste it - Enter your local address when prompted (default:
127.0.0.1:80) - The client will print your Public URL, e.g.,
https://your-server.com/s/<session>/— share this URL - View the session log:
https://your-server.com/s/<session>/_log
Notes:
- Long flags use kebab-case (e.g.,
--remote-host,--token-url,--allow-token-query).- Default auth uses header
Authorization: Bearer <token>. Query?token=...works only if server enables--allow-token-query.- For self-host without TLS, pass
--use-wss=falseso the client usesws://(the flag accepts an explicit boolean, e.g.,--use-wss=false).
If you want to run the dashboard without Docker:
-
Install dependencies:
cd frontend npm install -
Build the frontend:
npm run build
-
Set environment variables:
export BACKEND_BASE_URL=http://your-server.com:8080 export TUNLY_INTERNAL_KEY=your-internal-key
-
Start the frontend:
npm start
The frontend will be available at http://localhost:3000 and forward token requests to your Tunly server.
If building from source:
-
Self-host — run your own server, then point client to it.
- Start server (ephemeral token mode):
cargo run --bin tunly-server -- --bind 0.0.0.0:90002a) Start client (interactive, custom server):
cargo run --bin tunly-client -- --remote-host <server-ip-or-host>:9000 --use-wss=false --local 127.0.0.1:80802b) Start client (auto-fetch token):
cargo run --bin tunly-client -- --remote-host <server-ip-or-host>:9000 \ --use-wss=false \ --local 127.0.0.1:8080 \ --token-url http://<server-ip-or-host>:9000/token- Open the Public URL printed by the client, e.g.:
http://<server-ip-or-host>:9000/s/<session>/- Check recent paths accessed by visitors for that session:
http://<server-ip-or-host>:9000/s/<session>/_log
For a quick local test without internet:
- Start server with a fixed token:
cargo run --bin tunly-server -- --bind 127.0.0.1:9000 --token devtoken
- Start client (interactive) and connect over ws:
cargo run --bin tunly-client -- --remote-host 127.0.0.1:9000 --use-wss=false
When prompted, enter devtoken, then your local app address (e.g., 127.0.0.1:8080).
If you don't want to manage a static token, run the server without --token and without env TUNLY_TOKEN. The server will issue one-time tokens bound to the requester's IP via /token.
- Start server (ephemeral mode)
tunly-server.exe --port 9000 - Client auto-fetch token (advanced)
The client fetches a token from
tunly-client.exe --remote-host <vps-address>:9000 --token-url http://<vps-address>:9000/token/token(JSON or plain text) and connects via WebSocket using that token.
Notes:
- Tokens are one-time use, may be bound to the requester IP, and expire in ~5 minutes.
- Default auth is via header
Authorization: Bearer <token>;?token=query is disabled unless--allow-token-queryis set on the server. - If you prefer a fixed token, set
--token <value>or envTUNLY_TOKENon the server and keep usingconfig.txtor env on the client.
-
Fixed Token
- Server: run with
--token <value>or envTUNLY_TOKEN. - Client: paste token when prompted, or set it via
config.txt/TUNLY_TOKEN. - Best for interactive UX testing and simple setups.
- Server: run with
-
Ephemeral Token
- Server: run without
--token(issues one-time tokens via/token, tied tosession+IP, TTL ~5 minutes). - Client: use
--token-url http://<server>:<port>/tokenso the token matches the currentsidautomatically. - Manual prompt is not compatible with Ephemeral mode (will be rejected as invalid).
- Server: run without
- Cheap VPS: DigitalOcean, Vultr, Linode ($5/month)
- Free cloud: Oracle Cloud Free Tier, Google Cloud Free Tier
- Platform-as-a-Service: Render, Railway, Koyeb (easy deployment)
Tunly includes a built-in web dashboard for monitoring active tunnels and requests in real-time.
When running with the frontend (Docker Compose):
- Dashboard: http://localhost:3000/dashboard
When running server-only, use the API directly:
- Active tunnels:
GET http://localhost:8080/api/tunnels - Server stats:
GET http://localhost:8080/api/stats - Request history:
GET http://localhost:8080/api/requests
- Tunnels View: Monitor all active tunnels with session ID, request count, status, creation time, and last activity
- Requests Log: Real-time request history showing method, URI, HTTP status, and latency
- Statistics: Active tunnel count, total requests, uptime, and server status
- Settings: View server configuration and system information
| Endpoint | Method | Description |
|---|---|---|
/api/tunnels |
GET | List active tunnels with metadata |
/api/stats |
GET | Server statistics (active sessions, total requests, uptime) |
/api/requests |
GET | Recent request history (last 50 requests) |
All endpoints return JSON and auto-refresh every 5 seconds on the dashboard.
You can configure Tunly using environment variables. See the .env.example files in the root, backend/, and frontend/ directories for templates.
- Server env:
PORT(from platform, e.g., Render, Koyeb) — server listens on this port automatically.TUNLY_TOKEN— optional; if set, server uses fixed-token mode. If not set and--tokenis not provided, server uses ephemeral mode with/tokenissuance.TUNLY_INTERNAL_KEY— optional; if set, restricts/tokenaccess to requests providing this key in theX-Internal-Keyheader (prevents directcurlrequests to your backend).
- Client config:
config.txtwithtoken: <value>(tolerant totoken=/token:/tokenn).- Or env
TUNLY_TOKEN. - Or runtime fetch via
--token-url http://<server>:<port>/token(ephemeral mode).
- Frontend env:
BACKEND_BASE_URL— base URL of your Tunly backend (e.g.,https://<your-app>.koyeb.appor your custom domain). Used by the Next.js proxy routeapp/api/token/route.tsto call/token.TUNLY_INTERNAL_KEY— must match the server's key to allow the frontend to fetch tokens securely via the Next.js API route.
- Deploy on Koyeb:
- Source: Docker → Dockerfile path:
backend/Dockerfile - Health check:
GET /healthz - Environment:
TUNLY_TOKEN(optional): set for Fixed mode; leave empty for Ephemeral mode (/tokenenabled)PORT: injected automatically by Koyeb (no need to set)
- Optional: add a custom domain; Koyeb will provision TLS automatically
- Source: Docker → Dockerfile path:
/tokenrate limit: 10 requests per 60 seconds per IP- Ephemeral token TTL: ~5 minutes; single use; bound to requester's IP and session id
- Proxy request body limit: 2 MB
- Session idle TTL: ~10 minutes (inactive sessions are garbage-collected)
- Server logs each proxied request:
PROXY GET / -> 200 in 16ms (sid=abc123) - Client logs each local request it performs:
LOCAL GET /api -> 200 in 8ms - Session log page lists the last ~50 requests for a session:
- URL:
http://<server>/s/<session>/_log - Shows: Method, URI, Status, Duration (ms)
- Includes quick links to
/, /api, /blogfor quick checks
- URL:
GET /healthz— health checkGET /token— issue ephemeral token (available only in Ephemeral mode)GET /ws?sid=<session>— WebSocket entrypoint (useAuthorization: Bearer <token>header)GET /s/:sid/_log— recent paths accessed for the sessionANY /s/:sid/<...>— proxied traffic routed to the connected client
-
“Token is invalid or has expired.”
- Cause: Server is in Ephemeral mode but client used manual token prompt (token doesn’t match
sid). - Fix: Use
--token-url http://<server>:<port>/token, or run server with a fixed token (--token <value>) and then use manual prompt.
- Cause: Server is in Ephemeral mode but client used manual token prompt (token doesn’t match
-
Cannot connect over wss on localhost/self-host
- Cause: No TLS certificate for your self-hosted server.
- Fix: Add
--use-wss=falseto use plainws://during local testing.
- Token is the "password" for your tunnel.
- Don't share tokens with untrusted people.
- Change tokens regularly for extra security.
MIT License — free to use for commercial and non-commercial purposes.
