You've got a service running on a machine behind NAT, and something outside needs to reach it. ngrok makes that one command, which is why it became the default answer for "expose my localhost." It still is, honestly — for one port, on one box, managed for you. That's not a knock; it's the bar.
The strain shows up later. When "expose my localhost" becomes "let my agents reach each other," the tunnel-per-service model starts charging busywork: a new tunnel per service, per machine, per environment, hostnames to keep straight, a client process per endpoint to babysit. If you're searching for a self-hosted alternative to ngrok, the real question isn't which tunnel server to run — it's whether a tunnel is the right shape at all.
This post walks the options honestly: self-hosted tunnel servers, edge tunnels that stay managed, and the overlay shape that treats reachability as a property of the node instead of a per-port hack. One of those three is probably what you actually want, and it depends on what's doing the reaching.
Is a self-hosted alternative to ngrok even what you need?
Before picking software, separate two jobs that get conflated:
- Exposing a port. A human or a webhook wants to hit one service on one machine. The deliverable is a URL.
- Keeping nodes reachable. Agents, workers, and devices need to find each other, in both directions, across networks they don't control. The deliverable is an address that doesn't change when the network does.
ngrok is a superb answer to the first. For the second, it's a tool you'd have to assemble into a mesh yourself. Most "self-hosted ngrok alternatives" fall into three families, and each maps to one of those jobs.
Self-hosted tunnel servers: frp and friends
If you want the ngrok model without the managed part, you run a tunnel server on a box with a public IP. frp is the mature, widely-deployed option: a frps server, frpc clients, and a config file per exposed service.
# frps.toml
bindPort = 7000
# frpc.toml
serverAddr = "203.0.113.10"
serverPort = 7000
[[proxies]]
name = "agent-api"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8080
remotePort = 8080
You get full control: your own domain, your own auth, no third party in the path. Other members of this family — bore, localtunnel, a self-hosted ngrok agent/server pair — work the same way: a public rendezvous, a reverse connection from behind NAT, traffic forwarded to a local port.
What this family shares with ngrok: every exposed service is a separate mapping you define, authenticate, and maintain. Fine for a handful of ports. If you have services on many machines, the config grows with every service, and the server box becomes a piece of infrastructure you own — patching, capacity, uptime all on you. That's the honest trade for self-hosting.
Edge tunnels that stay managed: Cloudflare Tunnel
Cloudflare Tunnel (cloudflared) keeps the "managed for you" property of ngrok but inverts the deployment: instead of opening an inbound port, a daemon on your machine makes an outbound connection to Cloudflare's edge, and traffic arrives over that. It's genuinely good — free for a lot of use, no inbound firewall rules, and it solves the "what's my public IP" problem by not having one.
cloudflared tunnel create agent-api
cloudflared tunnel route dns agent-api agent-api.example.com
cloudflared tunnel run agent-api
The trade is different from frp: you're not self-hosting the control plane, and the model is still per-service hostnames. Each tunnel maps a public hostname to a local service. It's the tunnel model wearing a nicer coat — which is exactly right if your job is job #1 above.
The overlay shape: one address instead of one tunnel per port
The third family stops tunneling per port and instead gives every node a stable address on a private network that spans the internet. The classic versions are WireGuard-based meshes and self-hosted Tailscale control planes (Headscale). Every machine gets an address, traffic is encrypted end-to-end, and anything can reach anything that's authorized — no per-service mapping, no inbound ports.
For agent traffic specifically, there's a variant worth knowing: overlays built for agents rather than humans. Pilot Protocol is an open-source overlay network where each agent gets a permanent virtual address that survives restarts, IP changes, and moves between clouds. Transport is encrypted UDP tunnels (X25519 key exchange with AES-GCM), and NAT traversal is handled with STUN, hole-punching, and relay fallback — so agents behind NAT are reachable without a public box. Trust is explicit: a per-peer handshake that both sides approve, decoupled from membership, unlike a VPN where joining implies trust. Discovery goes through a rendezvous registry and nameserver, so an agent is found by name rather than by a forwarded port.
Where it diverges from the tunnel families is the tooling on top: a local app store of agent-native capabilities (pilotctl appstore catalogue → install → call), an MCP server, and SDKs for Go, Python, Node, and Swift. The install is a single command on any box an agent runs on. It's an open-source project (AGPL-3.0, Go standard library only) with 243k+ agents and users on the network.
Be clear-eyed about it though: an overlay is not a drop-in ngrok replacement. If the job is "give this one web UI a public URL for a human," a tunnel (frp, cloudflared, or ngrok itself) is the right tool — simpler, purpose-built, zero mental overhead. The overlay pays off when the job is the second one: many nodes, many services, agents calling agents, addresses that need to outlive the network they're on.
Which one do you actually want?
- One service, one URL, don't think about it → ngrok, or Cloudflare Tunnel if you want it managed and free.
- Full control of the rendezvous, modest scale → frp or a self-hosted ngrok server. You own the config and the box.
- A private network across your machines, humans and agents alike → WireGuard-based mesh or Headscale.
- Agents that must find each other across NAT, with trust and discovery built in → an agent-native overlay like Pilot Protocol.
There's no winner here; there's a fit. The mistake is reaching for a tunnel when what you actually have is a networking problem — and realizing that distinction is the whole game.
If you want to kick the tires on the overlay option, the install line is short enough to paste into a container build:
curl -fsSL https://pilotprotocol.network/install.sh | sh
Run pilotctl appstore catalogue afterward and you'll see what "discover, install, call" looks like for agent tooling. Either way — tunnel or overlay — you now know which question you were actually asking.
Top comments (0)