localhostly is a Pinggy alternative with a URL that stays
If you are leaving Pinggy, here is what you get instead: one public HTTPS URL that keeps its name for the whole session and comes back after a short drop. Run npx localhostly 3000 and port 3000 answers at https://brave-amber-fox.localhostly.iblabs.io, behind NAT. The account asks for an e-mail: 2 tunnels, 1000 requests per minute and 25 GiB a month.
Get your token Install localhostly
$ npx localhostly 3000 https://brave-amber-fox.localhostly.iblabs.io → http://127.0.0.1:3000 Ctrl+C to stop. Requests: 21:14:03 GET / 200 12ms 21:14:05 POST /api/webhook 201 31ms
The account in numbers
The five numbers below are the account's limits; traffic is counted in both directions, and the month starts again on the 1st, in UTC. At a limit, the visitor gets a 429 page that names it and says when it lifts.
- 2 tunnels open at the same time on one account
- 1000 requests per minute, counted over a sliding 60-second window
- 25 GiB of traffic a month, both directions added up
- 1 e-mail address to enter at sign-up
- 429 past a limit, with the
Retry-Afterheader set
One command, one public URL
Nothing is downloaded by hand: npx localhostly 3000 fetches the agent from npm and runs it. The agent opens one outgoing WebSocket connection to the localhostly server and keeps it, so no port is opened on your router or firewall. When a visitor loads your URL, the server finds the name in the address, hands the request to that connection, and the agent plays it against your local port and streams the reply back. HTTP and WebSocket both go through.
- Get a token. Type your e-mail in the sign-up form; the token is shown on the page, and no confirmation e-mail is sent.
- Save it.
localhostly login <token>stores it in~/.localhostly/config.json, readable by your user only. - Open the tunnel.
npx localhostly 3000, orlocalhostly 3000once installed withnpm i -g localhostly. The first line is the public URL; below it, one line per request with time, method, path, status and milliseconds.
Your app gets the visitor's method, path, headers and body, plus X-Forwarded-For, X-Forwarded-Proto: https and X-Forwarded-Host; --host myapp.test:3000 points the tunnel at another address and sets that Host. The name is 3 English words drawn at random, adjective, color and animal, and nobody picks it.
The URL you get at start is the URL until you press Ctrl+C
The name is drawn once, when the agent connects, and the URL printed on the first line is where every request after it arrives, for as long as the connection holds. The server pings the agent every 30 seconds and closes the connection after 60 seconds without an answer; in a measured run, a tunnel idle for 620 seconds stayed up. Ctrl+C is what closes it.
When the connection drops on the way, the agent reconnects by itself, waiting 1 second and doubling up to 60, and asks for its name back; the server keeps that name for it for 10 minutes, so the URL answers again with nothing changing on the visitor's side. After a longer drop the agent comes back under a new name and prints name changed.
Webhooks and API clients get no warning page
The first visit from a browser shows a warning page that says the site is served from someone's own computer; its button sets a cookie that keeps the page away for 24 hours. A webhook delivery, a curl call or any other API client is not a browser, so it never sees that page: the request goes straight to your app, and a client that cannot click sends the X-Localhostly-Skip-Warning header instead.
Register https://<name>.localhostly.iblabs.io/<your path> with the webhook provider; each delivery prints a line in your terminal with method, path, status and time, and a delivery that arrives while your app is down gets a 502 page saying so.
Streaming responses and big uploads pass through
Bodies move in pieces of up to 64 KiB, in both directions, so no request is ever held whole in memory; in the measured run a 20 MiB upload crossed the tunnel with peak memory under 100 MiB, and a 20 MiB download reached a slow visitor piece by piece.
A response that your app writes over time reaches the visitor as it is written: Server-Sent Events sent at 0, 3 and 6 seconds arrived at 0.6, 3.6 and 6.6 seconds in a measured stream. The one clock is on the first byte, 60 seconds; after it, a download takes as long as it takes. A visitor's WebSocket is handed to your app, and the upgrade completes once your app accepts it with the subprotocol it picked.
Lock the tunnel with a password or an IP list
--password puts a login box in front of every visitor; leave the value empty and localhostly makes a 12-character password and prints it in your terminal only. The Authorization header the visitor types never reaches your app.
--allow 203.0.113.7,10.0.0.0/8 admits those IP addresses or ranges and nobody else, IPv4 or IPv6, up to 64 entries: any other address gets a 403 page showing the address the server saw, and its WebSocket upgrade gets 403 as well. A tunnel with --allow skips the warning page, and with --password on top a visitor needs the address and the password.
- The server's log line per visit holds method, path without the query string, status, time, tunnel name and account, never a body or a cookie.
- On our side your token exists only as its SHA-256 hash.
- HTTPS ends at the localhostly load balancer: the server sees and relays your traffic, which is not end-to-end encryption.
Keep it running as a system service
localhostly service install 3000 makes the tunnel a service of the system: systemd on Linux, launchd on macOS, a Windows service. The service reconnects on its own, keeps the name it was given, asks for it again after a reboot and keeps trying if the server turns it down; localhostly service status says whether it is running and localhostly service uninstall removes it.
Install
localhostly runs on Node.js 24 or newer.
-
Try it without installing:
npx localhostly 3000 -
Install it once:
npm i -g localhostly
Both lines run the same agent from the npm registry. Save your token with localhostly login <token>; localhostly 3000 then opens the tunnel.
Get your token
Sign-up takes an e-mail address and nothing else: the token is shown on the sign-up page, no confirmation e-mail is sent, and localhostly login <token> keeps it on your computer.