Your app runs on http://localhost:3000 and something out on the internet —
a teammate, a phone, a webhook sender, an OAuth redirect — needs to reach it. Here are the
free ways to do that, with the real trade-offs of each, and one important question to ask
first: do you actually need to expose anything? (If the traffic is webhooks, the
answer is usually no — jump to that.)
$ cloudflared tunnel --url http://localhost:3000
Prints a public https://….trycloudflare.com URL that proxies to your port.
Good: no account, no signup, solid infrastructure.
Trade-offs: the URL is random and changes every run, so anything configured
with it (webhook providers, OAuth apps) breaks on restart. A stable named tunnel needs a
Cloudflare account and your own domain.
$ ngrok http 3000
Good: mature agent, request inspector, your URL is a stable
auto-assigned dev domain on today's free plan.
Trade-offs: account + authtoken required before the first request; free
monthly quotas (1 GB transfer, 20,000 HTTP requests); a browser-warning interstitial
that automated callers trip over (ERR_NGROK_6024 and
friends); capped concurrent agent sessions.
$ ssh -R 80:localhost:3000 nokey@localhost.run
Services like localhost.run and serveo speak plain SSH, which you already have.
Good: zero install, no account for basic use.
Trade-offs: random URLs per session and small-operator reliability; the
same class of pain as localtunnel, whose password interstitial blocks programmatic senders
outright (details).
$ tailscale funnel 3000
Publishes the port on the public internet at your node's stable
….ts.net name.
Good: URL never changes, real TLS, free tier is generous.
Trade-offs: you need Tailscale installed and an account; Funnel must be
enabled for the tailnet; public listeners are limited to ports 443, 8443 and 10000. Best
when you already run Tailscale anyway.
The Ports panel can make a forwarded port public on a devtunnels.ms URL.
Good: zero extra tools if you live in VS Code.
Trade-offs: requires signing in with GitHub; visibility defaults to
private (callers must authenticate — webhook senders can't); tied to your editor session,
not scriptable.
Every option above exposes a live port on your machine to the internet and dies (or breaks its URL) when the process stops. That's the right tool for demoing an interactive app to a human. But if the thing that needs to reach you is a webhook or callback — GitHub, Stripe, Shopify, Telegram, a payment provider — you can skip exposure entirely: give the provider a stable capture URL that's always up, and pull deliveries down to localhost yourself.
$ curl -s https://hookden.pages.dev/cli -o hookden && chmod +x hookden $ ./hookden new # → stable /h/… URL, configure the provider once $ ./hookden relay YOUR_BIN http://localhost:3000
Each delivery is re-delivered to your local server with the original method, sub-path,
query, headers and a byte-identical body — HMAC signatures like GitHub's
X-Hub-Signature-256 still verify. The public URL never rotates and keeps
accepting deliveries while your laptop is asleep; the relay replays what you missed when
you're back. Outbound polling only — nothing on your machine is reachable from the
internet, so it works behind NAT, VPNs and corporate proxies where tunnels are blocked.
Full walkthrough.
Honest scope: webhook-style HTTP traffic only — it can't serve a website or WebSocket app, delivery lags a couple of seconds behind the original, and bodies over 100 KB are truncated on relay (the CLI warns you).
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new