How to expose localhost:3000 to the internet
ngsrv creates a public preview link for your local service. This is the short, copy-pasteable version for the single most common case: you have something running on localhost:3000 and you need a public HTTPS URL pointing at it.
ngsrv helps developers expose local services, share preview links, test webhooks, and get feedback faster. Total time: about ninety seconds.
TL;DR
# 1. Install (macOS / Linux)
curl -fsSL https://get.ngsrv.com | bash
# 2. Authenticate (paste the token from your ngsrv dashboard)
ngsrv token <YOUR_TOKEN>
# 3. Expose localhost:3000
ngsrv http 3000
You'll get a public HTTPS URL like https://furry-otter-1842.tnl.ngsrv.com. Hit it from a browser, a webhook provider, your phone — it all routes to your local server.
What's running on port 3000?
Port 3000 is the default for a lot of stacks:
- Next.js —
npm run dev - Express / Fastify / Koa — most starter templates listen on 3000
- Rails —
bin/rails s - Django — sometimes (the actual default is 8000, but lots of dev scripts override)
- Create React App — historical default
The procedure is the same regardless. ngsrv doesn't care what the upstream is — it just forwards.
Step 1 — Install
Pick your OS:
# macOS (Homebrew, recommended)
brew install ngsrv/tap/ngsrv
# macOS / Linux (curl)
curl -fsSL https://get.ngsrv.com | bash
# Windows (PowerShell)
irm https://get.ngsrv.com/windows | iex
Verify:
ngsrv --version
Step 2 — Authenticate
Sign up at ngsrv.com/register (free, no card). Copy your token from the dashboard and:
ngsrv token <YOUR_TOKEN>
This writes ~/.ngsrv/config.yaml. One time per machine.
Step 3 — Run the tunnel
With your dev server already running on port 3000:
ngsrv http 3000
Output looks like:
ngsrv (v2.3.1)
session #a83b
forwarding https://furry-otter-1842.tnl.ngsrv.com -> http://localhost:3000
region eu-west-1
status online · 0ms
press Ctrl-C to stop
That https://... URL is now public.
Step 4 (optional) — Reserve a stable subdomain
Random subdomains rotate each session. If you're sharing this with a client or wiring it into a webhook provider, reserve a name:
ngsrv http 3000 --subdomain my-app
# -> https://my-app.tnl.ngsrv.com
Reserved subdomains live across restarts on Pro and above.
Step 5 (optional) — Lock it down
Don't want strangers on the internet poking your dev server? Drop a ngsrv.yml next to your code:
tunnels:
- name: web
port: 3000
subdomain: my-app
security_policies:
- type: header_required
header: "X-Preview-Token"
value: "swap-me"
Then ngsrv run ngsrv.yml. Anything without the header gets a 401 at the edge — your local server never sees the request.
When things go wrong
connection refused — Your dev server isn't actually running on port 3000. Run lsof -i :3000 (macOS / Linux) or netstat -ano | findstr :3000 (Windows) to confirm.
websocket disconnected, reconnecting… — Network blip. ngsrv reconnects automatically. If it persists, check your corporate proxy / VPN.
401 invalid token — Your CLI token is missing or expired. Re-run ngsrv token <YOUR_TOKEN> from the dashboard.
Pages render but assets fail. Most likely Next.js / Vite refusing the host. For Next.js, add the ngsrv hostname to experimental.allowedDevOrigins. For Vite, set server.allowedHosts in vite.config.ts.
What this gives you over a deploy-to-staging loop
- No CI wait. The link is live the moment your dev server is.
- Real debugger. Breakpoints work, hot reload works, your editor stays in charge.
- Stable URLs. Reserved subdomains and custom domains don't change when you restart.