ngrok solves a real problem: your service is behind a NAT or firewall, and you need a public URL for it. A lightweight client on your machine makes an outbound connection to a relay server with a public IP, and traffic arriving at that relay is forwarded back through the tunnel to your local port. ngrok provides the relay. In the self-hosted model, you provide the relay on a VPS or any machine with a routable address, and you run open-source software on both ends.

Why Look for Alternatives

Several concrete limitations push people off ngrok's hosted tiers:

  • The free plan caps bandwidth at 1 GB per month with one active endpoint, which disappears quickly during active development.
  • The Personal plan ($10/month) only gives 5 GB, with $0.10/GB overages that accumulate fast.
  • Free accounts get random subdomain URLs that change on every restart, breaking webhook integrations that need stable endpoints.
  • No UDP support, which rules out game servers, VoIP applications, and IoT devices using protocols like CoAP or DTLS.
  • The server has been closed source since version 1 (over six years ago), so what runs on their relay is not auditable.

If you already have a VPS (and most developers do, even a $5/month instance), the self-hosted option is strictly better in every way that matters: no bandwidth caps, no rate limits, no interstitial pages, no closed-source relay seeing your traffic, and no monthly fee for a feature that costs you a few minutes to set up. You get full UDP support, stable custom domains, and complete control over the relay. Paying for ngrok on top of a VPS you already rent is paying twice for the same public IP.

If you already own a box with a public IP, ngrok sells you a feature you already have: a public IP. Self-hosting the relay closes that gap in a few minutes.

The options below are all open source, free to run, and cover the range from single-binary minimalism to full zero-trust overlays. "Free to run" means you provide a box with a public IP as the relay: a cheap VPS, a home server with a forwarded port, or any machine with a routable address.

frp

frp (Fast Reverse Proxy) is the established default with over 100k GitHub stars and active maintenance. Written in Go, it covers HTTP, HTTPS, TCP, UDP, and peer-to-peer modes (STCP, XTCP). Configuration is TOML on both the server (frps) and client (frpc). It has a built-in dashboard and has been battle-tested at scale for years. If you want one tool that covers every protocol and use case without surprises, this is it.

# server (frps.toml)
bindPort = 7000

# client (frpc.toml)
serverAddr = "your.server.com"
serverPort = 7000

[[proxies]]
name = "web"
type = "http"
localPort = 3000
customDomains = ["app.your.server.com"]

rathole

rathole is frp's philosophy in Rust: high throughput, low memory, a single static binary. It targets constrained environments (routers, embedded hardware, minimal VPSes) and supports TCP and UDP. The config format is TOML. Reach for this when resource use matters or when you want an auditable, minimal codebase.

# server (server.toml)
[server]
bind_addr = "0.0.0.0:2333"

[server.services.myapp]
token = "your-secret-token"
bind_addr = "0.0.0.0:8080"

# client (client.toml)
[client]
remote_addr = "your.server.com:2333"

[client.services.myapp]
token = "your-secret-token"
local_addr = "127.0.0.1:3000"

sish

sish fits naturally into a setup already built around SSH keys. There is no bespoke client binary: you expose a local port using plain ssh -R, and the sish server handles HTTP(S), WebSocket, and TCP forwarding with automatic TLS via Let's Encrypt. Custom domains are supported. Because the transport is SSH, your existing key model (ed25519, FIDO2 hardware keys) carries over without any additional tooling.

# expose a local HTTP server on port 3000
ssh -p 2222 -R myapp:80:localhost:3000 your.server.com

# expose a TCP port
ssh -p 2222 -R 9000:localhost:9000 your.server.com
docker run -it -p 80:80 -p 443:443 -p 2222:22 \
  -v /etc/letsencrypt:/etc/letsencrypt \
  antoniomika/sish:latest \
  --ssh-address=:2222 \
  --http-address=:80 \
  --https-address=:443 \
  --https=true

Serveo

Serveo is similar to sish in that the client is plain SSH, but it is a hosted service rather than something you self-host. No account is required; a custom subdomain is requested via the ssh command itself. It is the fastest path to a public URL if you have SSH and nothing else. The tradeoff is reliability: outages are common, and you have no control over the relay. Useful for quick demos, not for anything you depend on.

# expose local port 3000
ssh -R myapp:80:localhost:3000 serveo.net

bore and chisel

bore (Rust, TCP only) and chisel (Go, HTTP/WebSocket transport) are the minimalists. bore is dead simple: one command, one port, no config files. chisel works well when egress is locked down to HTTP/HTTPS, because it tunnels over WebSocket and passes through corporate HTTP proxies. Reach for either when you need one port exposed quickly with nothing to configure.

bore's simplicity is not a weakness. My 14-year-old, running a Minecraft server on a custom Android build, figured it out without documentation, without an account, and without touching router settings, replacing Aternos (free but queue-based, shuts down when idle) with a server that stays up and responds instantly. If the concept is simple enough to explain in one sentence, bore is probably the right tool.

# bore: expose local port 8080
bore local 8080 --to bore.pub

# chisel: server
chisel server --port 9090 --reverse

# chisel: client
chisel client your.server.com:9090 R:8080:localhost:8080

Boringproxy

Boringproxy is a self-hosted reverse proxy where the single executable contains both server and client. It automates HTTPS certificates via Let's Encrypt, handles NAT traversal, and provides a web interface for managing tunnels. It uses SSH internally for the secure transport. The target audience is self-hosters running services like Nextcloud or Jellyfin on home networks who want a dashboard without configuring frp by hand.

inlets

inlets is designed for Kubernetes-native environments. The open-source edition does HTTP/S tunnels with automatic Let's Encrypt certificates and custom domains. If your services run in a cluster behind a load balancer and you need a straightforward way to expose them publicly or connect clusters, inlets fits that model better than the general-purpose tools above. Note: the Pro version, which adds TCP forwarding and the Kubernetes operator, is commercial.

zrok

zrok is the sovereignty-maximalist pick. Built on OpenZiti (a programmable zero-trust network overlay), it supports both public sharing (ngrok-style) and private sharing (peer-to-peer, never touching a public relay). Apache 2.0. Running your own instance means deploying the full OpenZiti backend, which is real work. But if zero-trust is the substrate you want rather than a public-edge forwarding model, this is the correct architecture and the private-share model has no equivalent in the other tools.

Pangolin

Pangolin by Fossorial is the newest entrant worth watching. It is a self-hosted tunneled reverse proxy with a WireGuard-based agent and a web UI for managing exposed services across multiple machines. More product than primitive: it has a dashboard, user management, and a straightforward setup. If you want a fleet view over several tunneled services without stitching together config files, this is the most complete package currently available.

netcat

netcat (nc) is socat's older, simpler ancestor. It reads and writes raw TCP or UDP and has been shipping on Unix systems since 1995. For a one-shot port relay it still works, but it handles one connection at a time with no forking, which makes it unsuitable for anything serving more than a single client. It is the tool you reach for when socat is not installed and you just need to verify that two machines can talk to each other on a given port.

# one-shot relay (single connection, then exits)
nc -l 8080 | nc internal-host 3000

# check if a port is reachable
nc -zv relay.host 2222

stunnel

stunnel was the standard answer for adding TLS to a plaintext service before Let's Encrypt made automatic certificates trivial. It wraps any TCP connection in SSL/TLS with a config file, no code changes to the service required. It was widely used in the 2000s to front MySQL, SMTP, and POP3 with TLS when the services themselves had no native support. Today most services handle TLS natively and certbot or Caddy handles the certificates, so stunnel's role has shrunk considerably, but it still appears in legacy setups where adding TLS elsewhere is not an option.

; /etc/stunnel/stunnel.conf
[mysql-tls]
accept  = 3307
connect = 127.0.0.1:3306
cert    = /etc/stunnel/server.pem

socat

socat (SOcket CAT) predates the modern tunnelers on this list by well over a decade and deserves mention for historical reasons. It is a raw bidirectional relay between two endpoints: TCP sockets, Unix sockets, files, serial ports, or anything else the OS can open. For forwarding a port on a machine you can already reach, it still works perfectly and ships on almost every Linux system by default.

# forward local port 8080 to a remote host:port
socat TCP-LISTEN:8080,fork TCP:internal-host:3000

# expose a local port through an SSH tunnel (classic combo)
ssh -R 9000:localhost:3000 user@relay.host &
socat TCP-LISTEN:9000,fork TCP:localhost:3000

The important distinction: socat is a relay, not a NAT traversal tool. It moves bytes between two endpoints you can already address. It does not punch through firewalls, does not create outbound tunnels, and does not give you a public URL. If both sides are routable, socat is the simplest possible answer. If one side is behind a NAT, you need one of the tools above and socat can be part of the chain, nothing more.

Cloudflare Tunnel

Cloudflare Tunnel (cloudflared) is not self-hosted: your traffic goes through Cloudflare's edge, not a relay you control. The trade-off is that you get Cloudflare's global network, DDoS protection, and automatic TLS without running any server-side component yourself. The free tier is genuinely usable, there are no bandwidth caps, and the client is a single binary. The requirement is a domain managed by Cloudflare, which makes it a non-starter if you prefer to keep your DNS elsewhere. If you are already in the Cloudflare ecosystem and want the simplest possible setup without a VPS, this is the most polished option in this space.

# authenticate once
cloudflared tunnel login

# create a named tunnel
cloudflared tunnel create my-tunnel

# route traffic and run
cloudflared tunnel route dns my-tunnel app.yourdomain.com
cloudflared tunnel run --url localhost:3000 my-tunnel

Tailscale Funnel

Tailscale Funnel exposes a local port to the public internet over the Tailscale mesh. If you already run Tailscale for private networking between your machines, Funnel adds one command and your service gets a stable public HTTPS URL on a ts.net subdomain. The free personal tier covers it. Like Cloudflare Tunnel, the traffic goes through Tailscale's infrastructure rather than a relay you own. Unlike Cloudflare Tunnel, it does not require a custom domain. The limitation is port choice: Funnel supports ports 443, 8443, and 10000 only.

# expose local port 3000 publicly (HTTPS, stable ts.net URL)
tailscale funnel 3000

Localtunnel

Localtunnel is the zero-friction option: no account, no install beyond npm, one command. HTTP and HTTPS only, random URLs, reliability that reflects its free-hosted nature. It is not production-grade, but it is the fastest way to share a local port with someone who needs to see it right now.

npx localtunnel --port 3000

Which One

ToolModelWhy pick it
frpself-hostedone tool, all protocols (including UDP), proven at scale
ratholeself-hostedfrp in Rust, smaller and faster, good for constrained hardware
sishself-hostedno new client binary, SSH key model, automatic TLS
Serveohostedhosted sish equivalent, zero setup, not reliable enough for anything real
bore / chiselself-hostedone port, one command, nothing to configure
Boringproxyself-hostedweb UI, single binary holds both server and client
inletsself-hostedKubernetes-native, TCP and HTTP, commercial for the full feature set
zrokself-hostedzero-trust overlay, private peer-to-peer sharing, OpenZiti substrate
Pangolinself-hostedweb UI, WireGuard agent, fleet management across machines
Cloudflare Tunnelhosted edgefree tier, no VPS needed, requires a CF-managed domain
Tailscale Funnelhosted meshfree personal tier, ports 443/8443/10000 only
netcatrelay onlyone-shot, single connection, no forking, use for connectivity checks
stunnelrelay onlyTLS wrapper for plaintext services, still relevant in legacy setups
socatrelay onlyraw socket relay, both sides must be routable, no NAT traversal
LocaltunnelhostedHTTP only, zero install, for quick sharing only

ngrok gives you a public URL for a service stuck behind a NAT: a client on your box dials out to a relay with a public IP, and the relay forwards inbound traffic back down the tunnel. The catch is the relay is theirs, and the free tier is tight: 1 GB/month, one endpoint, random URLs that change on restart and break webhooks, no UDP, and a closed-source server you cannot audit. If you already rent a VPS, you already own a public IP, so self-hosting the relay is strictly better and takes minutes.

Paying for ngrok on top of a VPS you already rent is paying twice for the same public IP.

Self-hosted tunnels

These run open-source software on both ends; you supply a box with a public IP as the relay.

  • frp. Go, the default. HTTP, TCP, UDP, peer-to-peer, a dashboard, proven at scale. One tool that covers every case.
  • rathole. frp's idea in Rust: one static binary, low memory, TCP and UDP. Reach for it on routers or tiny VPSes.
  • sish. No client binary; you expose a port with plain ssh -R and the server does HTTP(S), TCP, and automatic TLS. Your SSH key model carries over.
  • bore / chisel. The minimalists: one port, one command. chisel tunnels over WebSocket, so it survives a corporate proxy that only allows 80/443.
  • Boringproxy, inlets, Pangolin. Single-binary or fleet tools with a web UI. inlets is Kubernetes-native; Pangolin uses a WireGuard agent to manage many tunnels.
  • zrok. Zero-trust on OpenZiti. Private shares go peer-to-peer and never touch a public relay; more setup, but no equivalent elsewhere.

Hosted, no relay to run

Cloudflare Tunnel and Tailscale Funnel route through someone else's edge, so there is nothing to host. Cloudflare needs a CF-managed domain and gives you DDoS protection and a free tier; Tailscale Funnel needs no domain but only opens ports 443, 8443, and 10000. Serveo and Localtunnel are the fastest path to a URL with just SSH or npm, but both are unreliable and fine only for a quick demo.

Not tunnels

socat and netcat are raw relays: they move bytes between two endpoints you can already reach, with no NAT traversal and no public URL. stunnel only wraps a plaintext service in TLS. Use these when both sides are routable, or as one link in a chain, not as the tunnel itself.


Sources