Public HTTPS URLs for your localhost in one command: every CLI flag, the Node API, a full safety guide for exposing local servers, and how to self-host your own relay.
Install it
Needs Node.js. Global install gives you the lt command anywhere; no install at all works too, via npx.
Point it at whatever's running
That's the entire tool. The URL is random each run unless you ask for a subdomain (next tab). Leave the terminal open — closing it, or Ctrl+C, ends the tunnel immediately.
The first-visit warning page
Open the URL in a browser and you'll hit a "Friendly Reminder" interstitial before your site loads — it's the public loca.lt service's own anti-abuse page, not your app, and only browsers see it (API/webhook calls skip it automatically). Click through once per browser/IP, or send the header below to skip it every time — handy for automated tests hitting the tunnel from a script.
The webhook loop: the actual reason most people install this
Stripe, GitHub, Twilio, Slack, and PayPal all need a public URL to POST events at — none of them can reach localhost:3000. Point the webhook config at your tunnel URL and every event lands on your dev machine, in your debugger, in real time.
Direction matters
The connection is outbound-initiated from your laptop, which is why no port forwarding or firewall rule is needed — same trick SSH reverse tunnels and ngrok use.
HTTPS is on the public side
The loca.lt URL is always HTTPS. Your local server can stay plain HTTP — the tunnel terminates TLS for you (that's also why it can't see or fix an invalid cert on your side without --allow-invalid-cert).
No account, no dashboard
Unlike ngrok, there's no login, no free-tier request cap you'll hit, and no web dashboard of past requests — also means no built-in request inspector or replay.
URLs aren't permanent
Stop lt and the URL is gone for good — someone else can be handed that exact subdomain next. Never bookmark one for later.
Every flag lt understands. Confirmed against the CLI source (bin/lt.js) — run lt --help to see the same list from your installed version.
| Flag | Example | What it does |
|---|---|---|
| -p, --port | lt --port 8000 | The only required flag: the local port to expose. Everything else is optional. |
| -s, --subdomain | lt --port 8000 --subdomain acme-preview | Request a specific name → acme-preview.loca.lt. First-come, first-served — someone else may already hold it, and it's never guaranteed, not even to you tomorrow. |
| -h, --host | lt --port 8000 --host https://tunnel.acme.dev | Point at a different relay server instead of the default public https://localtunnel.me — your own self-hosted server (Self-Host tab) or a team's private one. |
| -l, --local-host | lt --port 8000 --local-host myapp.test | Forward to a different local hostname instead of localhost, and rewrite the Host header to match — needed by dev servers (Vite, Webpack Dev Server) that reject requests unless the Host header matches what they expect. |
| --local-https | lt --port 8443 --local-https | Your local server is HTTPS (not HTTP) — tell the tunnel to speak TLS on the local hop too. |
| --local-cert | --local-https --local-cert ./cert.pem | Path to the local HTTPS server's certificate PEM, when it uses one the tunnel wouldn't otherwise trust. |
| --local-key | --local-https --local-key ./key.pem | Matching private key file for --local-cert. |
| --local-ca | --local-https --local-ca ./ca.pem | Certificate authority file, for a local dev cert signed by your own self-signed CA (mkcert, etc.). |
| --allow-invalid-cert | lt --port 8443 --local-https --allow-invalid-cert | Skip certificate validation entirely for the local hop — the quick way past a self-signed-cert error during local dev. Local dev only: it silently ignores cert/key/ca too, so don't reach for it out of laziness on anything real. |
| -o, --open | lt --port 8000 --open | Opens the tunnel URL in your default browser as soon as it's up. One less copy-paste. |
| --print-requests | lt --port 8000 --print-requests | Logs a line per incoming request to your terminal — method + path, nothing fancier. The closest thing to ngrok's request inspector this tool has; there's no HTML/JSON dashboard. |
| Env var form | PORT=3000 lt | Any flag can also be set as an environment variable — handy in a docker-compose service or a CI job where you'd rather not hardcode the command. |
| --help | lt --help | Prints this exact list from whatever version you actually have installed — flags do occasionally change between releases. |
The minimal version
With the options that matter
Wrapped around a server you're spinning up in the same script
The pattern for an end-to-end test suite: start the app, tunnel it, run whatever needs the public URL (a webhook simulator, a cloud browser service), then tear both down.
Handle the events, or a dead tunnel fails silently
The relay connection can drop — restarts, network blips, a rate limit. Without an error listener, an unhandled error can crash the process (standard Node EventEmitter behavior); without a close handler, you won't notice a test run went dark until it times out.
| Option / member | Type | Notes |
|---|---|---|
| port | number, required | Same as the CLI's --port. The only required option. |
| subdomain | string | Requested, not guaranteed — check tunnel.url after connecting to see what you actually got. |
| host | string, default https://localtunnel.me | The relay/broker server. Point at your own localtunnel-server instance here. |
| local_host | string | Forward to this hostname (and rewrite the Host header to it) instead of localhost. |
| local_https | boolean | Local server speaks HTTPS. |
| local_cert / local_key / local_ca | string (file path) | Certificate material for the local HTTPS hop. Ignored entirely if allow_invalid_cert is true. |
| allow_invalid_cert | boolean | Skip local TLS validation. Local dev only — see the CLI tab's warning, it applies identically here. |
| tunnel.url | string (readonly) | The assigned public URL. Read it after the returned Promise resolves — it isn't known beforehand even when you requested a subdomain. |
| tunnel.close() | method | Tears the tunnel down. Call it in a finally block or a test framework's afterAll — an orphaned tunnel from a crashed test run stays open on the relay until it independently times out. |
| 'request' event | event | Fires per incoming request with basic method/path info — the programmatic version of --print-requests. |
| 'error' event | event | Always attach a listener. An EventEmitter's unhandled error event throws and can crash the process. |
| 'close' event | event | Fires when the tunnel ends, whether you called .close() or the relay dropped it. |
- A tunnel URL has zero authentication by default. Anyone who has the link — because you sent it, because it leaked in a screenshot, because it got logged somewhere, or because someone guessed a short/common subdomain — can hit your local server exactly as if they were sitting at
localhost. If that server has no auth of its own, neither does the tunnel. - It's a relay you don't control. The default
https://localtunnel.meis a free, community-run, best-effort public service. Every request to your machine passes through it in plaintext-to-the-relay-then-reencrypted fashion (TLS terminates there). Don't send anything through it you wouldn't be comfortable a third-party operator technically being able to see. - Treat every tunnel as temporary and disposable — spin it up for the task, close it the moment you're done, never leave one running "just in case" overnight or over a weekend.
| Practice | Why | How |
|---|---|---|
| Never tunnel production or anything with real user data | The tunnel, the relay, and the public URL are all outside your normal security boundary — no WAF, no rate limiting, no access log you control, no SLA. | Tunnel a local copy or a seeded dev database only. If a client needs to see something real, deploy it properly (see the scp/rsync deploy section on the Linux cheatsheet) instead of tunneling prod. |
| Put a password in front of anything sensitive | The tunnel forwards every request as-is; it adds zero auth of its own. | Basic Auth (htpasswd, same pattern as the Linux cheatsheet's guide) in front of your dev server, or middleware that checks a shared secret header/query param before proxying through. |
| Rotate the subdomain like a secret, not a bookmark | A predictable or reused subdomain (--subdomain myapp every day) is guessable and gets bookmarked/logged by whoever you shared it with, past when you meant them to have access. | Let it be random by default for anything short-lived; only fix a subdomain when you specifically need a stable link for a limited window (a demo call), and close the tunnel right after. |
| Never put real API keys or secrets in a tunneled request/response for a demo | You don't control the relay's logs, and screen-shares/recordings of a tunnel session capture the URL bar too. | Use test-mode API keys (Stripe test keys, sandboxed OAuth apps) for anything you're about to expose publicly, even briefly. |
Check --local-host/binding before you assume "local" means private | If your dev server itself binds to 0.0.0.0 instead of 127.0.0.1, it's already reachable on your LAN — the tunnel just adds a second, public path to the same exposed server. | lsof -iTCP -sTCP:LISTEN -P | grep node to see what your dev server actually bound to; prefer 127.0.0.1 unless you specifically need LAN access too. |
| Validate webhook signatures even over a tunnel | Anyone who finds your webhook URL (guessed subdomain, leaked log line) can POST fake events at it. The tunnel doesn't verify anything about the sender. | Keep your provider's signature check (Stripe's stripe-signature header, GitHub's X-Hub-Signature-256) active in dev exactly as it would be in production — don't special-case it away "because it's just local". |
| Close it when you're done, not "eventually" | A forgotten tunnel from an old terminal tab is a live public entry point into your machine that nobody is watching. | Ctrl+C when finished. In scripts/CI, always tunnel.close() in a finally. Periodically run ps aux | grep localtunnel to check nothing's lingering. |
| Don't tunnel a database or admin panel directly | phpMyAdmin, Adminer, a Postgres/Redis admin UI, or a raw DB port tunneled out is one of the most common real-world exposure mistakes — search "exposed" + any of those tool names for why this matters. | Tunnel the application, not its infrastructure. If you truly need remote DB access, use an SSH tunnel to a server you control (see the SSH cheatsheet) with key-based auth, not a public relay. |
| Assume the URL will be scanned | Automated bots crawl *.loca.lt and similar tunnel-domain patterns looking for exactly the mistakes above. Being "obscure" isn't a security boundary. | Treat every tunnel as internet-facing from second one, not "probably fine for a few minutes". |
| For anything client-facing or long-lived, self-host your own relay | You get to control logging, TLS, and who can reach the relay at all — removes the "shared public service" trust question entirely. | Run localtunnel-server on your own VPS behind your own domain (see the Node API tab's host option) — a few commands, covered below. |
Needs wildcard DNS for the domain (every subdomain must resolve to the VPS) and a reverse proxy in front handling TLS — nginx with a wildcard cert (Let's Encrypt DNS-01 challenge) is the standard setup, same pattern as any other reverse-proxied Node app on that box.
localhost can't: put the URL on a phone over cellular data, not just the same Wi-Fi. Catches CORS/mixed-content/service-worker bugs a same-LAN test misses.
Host header doesn't match what they expect — the tunnel's public hostname trips this. Fix in the dev server config (Vite: server.allowedHosts; webpack-dev-server: allowedHosts), not by fighting localtunnel.
Vite 5+: allowedHosts: ['.loca.lt'] allows any loca.lt subdomain.
ssh -R, see the Linux cheatsheet's scp/rsync section for the SSH setup) to a server you own: full control, no third party at all, more setup.
lsof -iTCP:8000 -sTCP:LISTEN to confirm what's really listening before blaming the tunnel.
error/close events, or self-host.
localtunnel vs local-tunnelnpm info localtunnel before installing from a tutorial — several similarly-named packages exist on npm with different maintainers and different feature sets. The one this page documents is localtunnel (no hyphen), by the localtunnel GitHub org.
Official docs & further reading
The primary sources. localtunnel is a small, actively-maintained open-source project — when this page and the source disagree, the source wins.
localtunnel() function's options object, and the tunnel object's events and methods documented on the CLI and Node API tabs above.
github.com/localtunnel/localtunnel
localtunnel-server
The relay server source — what actually runs behind loca.lt, and what you clone to self-host your own (Safety Guide tab). Includes the Docker image and API endpoint reference.
github.com/localtunnel/server
npm package
Version history, weekly download counts, and the dependency tree — worth a glance before npm install -ging anything, tunnel tool or otherwise.
npmjs.com/package/localtunnel
Issue tracker
A community-maintained free service occasionally has downtime or a flaky subdomain — check here first if the public relay seems to be misbehaving before assuming it's your setup.
github.com/localtunnel/localtunnel/issues