Files
reflector/docsv2/tunnel-setup.md

6.4 KiB

Tunnel Setup (Self-Hosting Behind NAT)

Expose your self-hosted Reflector + LiveKit stack to the internet without port forwarding, static IPs, or cloud VMs using tunneling services.

Requirements

You need two tunnels:

Tunnel Protocol What it carries Local port Examples
TCP tunnel TCP Web app, API, LiveKit signaling (WebSocket) 443 (Caddy) playit.gg, ngrok, Cloudflare Tunnel, bore, frp
UDP tunnel UDP WebRTC audio/video media Assigned by tunnel service playit.gg, frp

Important: Most tunneling services only support TCP. WebRTC media requires UDP. Make sure your chosen service supports UDP tunnels. As of writing, playit.gg is one of the few that supports both TCP and UDP (premium $3/mo).

Architecture

Internet participants
    │
    ├── TCP tunnel (HTTPS)
    │   └── tunnel service → your machine port 443 (Caddy)
    │       ├── /v1/*          → server:1250 (API)
    │       ├── /lk-ws/*       → livekit-server:7880 (signaling)
    │       └── /*             → web:3000 (frontend)
    │
    └── UDP tunnel
        └── tunnel service → your machine port N (LiveKit ICE)

Setup

Step 1: Create tunnels with your chosen service

Create two tunnels and note the public addresses:

  • TCP tunnel: Points to your local port 443
    • You'll get an address like your-tunnel.example.com:PORT
  • UDP tunnel: Points to a local port (e.g., 14139)
    • You'll get an address like udp-host.example.com:PORT
    • The local port must match the public port (or LiveKit ICE candidates won't match). Set the local port to the same number as the public port assigned by the tunnel service.

Step 2: Run the setup script

./scripts/setup-selfhosted.sh <mode> --livekit --garage \
  --tunnels <TCP_ADDRESS>,<UDP_ADDRESS>

Example:

./scripts/setup-selfhosted.sh --cpu --livekit --garage \
  --tunnels my-tunnel.example.com:9055,udp-host.example.com:14139

Or use separate flags:

./scripts/setup-selfhosted.sh --cpu --livekit --garage \
  --tunnel-tcp my-tunnel.example.com:9055 \
  --tunnel-udp udp-host.example.com:14139

The script automatically:

  • Sets all URLs (API, frontend, LiveKit signaling) to the TCP tunnel address
  • Configures LiveKit with the UDP tunnel port and resolved IP for ICE candidates
  • Enables Caddy with self-signed TLS (catch-all on port 443)
  • Saves tunnel config for re-runs

Step 3: Start the tunnel agent

Run your tunneling service's agent/client on the same machine. It must be running whenever you want external access.

Step 4: Access

Share https://<TCP_TUNNEL_ADDRESS> with participants. They'll need to accept the self-signed certificate warning in their browser.

Flag Reference

Flag Description
--tunnels TCP,UDP Both tunnel addresses comma-separated (e.g., host:9055,host:14139)
--tunnel-tcp ADDR TCP tunnel address only (e.g., host.example.com:9055)
--tunnel-udp ADDR UDP tunnel address only (e.g., host.example.com:14139)

Tunnel flags:

  • Imply --caddy (HTTPS required for browser mic/camera access)
  • Are mutually exclusive with --ip and --domain
  • Are saved to config memory (re-run without flags replays saved config)

UDP Port Matching

LiveKit advertises ICE candidates with a specific IP and port. The browser connects to that exact address. If the tunnel's public port differs from the local port, ICE will fail.

Correct setup: Set the tunnel's local port to match its public port.

Tunnel assigns public port 14139
  → Set local port to 14139
  → LiveKit listens on 14139 (udp_port in livekit.yaml)
  → Docker maps 14139:14139/udp
  → ICE candidates advertise tunnel_ip:14139
  → Browser connects to tunnel_ip:14139 → tunnel → local:14139 → LiveKit

If your tunneling service doesn't let you choose the local port, you'll need to update livekit.yaml manually with the assigned ports.

TLS Certificate Warning

With tunnel services on non-standard ports (e.g., :9055), Let's Encrypt can't auto-provision certificates (it requires ports 80/443). Caddy uses tls internal which generates a self-signed certificate. Participants will see a browser warning they must accept.

To avoid the warning:

  • Use a tunnel service that provides port 443 for TCP
  • Or use a real domain with --domain on a server with a public IP

Compatible Tunnel Services

Service TCP UDP Free tier Notes
playit.gg Yes (premium) Yes (premium) Limited $3/mo premium. Supports both TCP + UDP.
ngrok Yes No Limited TCP only — needs a separate UDP tunnel for media
Cloudflare Tunnel Yes No Yes TCP only — needs a separate UDP tunnel for media
bore Yes No Self-hosted TCP only
frp Yes Yes Self-hosted Requires your own VPS to run the frp server
Tailscale Funnel Yes No Free (3 nodes) TCP only, requires Tailscale account

For a full self-contained setup without a VPS, playit.gg (TCP + UDP) is currently the simplest option.

Limitations

  • Latency: Adds a hop through the tunnel service's relay servers
  • Bandwidth: Tunnel services may have bandwidth limits on free/cheap tiers
  • Reliability: Depends on the tunnel service's uptime
  • Certificate warning: Unavoidable with non-standard ports (see above)
  • Single UDP port: Tunnel mode uses a single UDP port instead of a range, which limits concurrent WebRTC connections (~50 participants max)
  • Not production-grade: Suitable for demos, small teams, development, and privacy-first setups. For production, use a server with a public IP.

Comparison

Approach Cost Setup Data location Port forwarding needed
Tunnel (this guide) $0-3/mo Low Your machine No
Cloud VM $5-20/mo Low Cloud provider No
Port forwarding $0 Medium Your machine Yes (router config)
VPN mesh (Tailscale) $0 Low Your machine No (VPN peers only)