localhostly is a free LocalXpose alternative
If you are leaving LocalXpose, here is what you get instead: one command, one public HTTPS URL and a free account with no paid plan behind it. Run localhostly 3000 and port 3000 answers at https://brave-amber-fox.localhostly.iblabs.io, behind NAT. Sign-up asks only for an e-mail address: 2 tunnels, 1000 requests per minute and 25 GiB a month.
Get your token Install localhostly
$ 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 free account in numbers
There is one account and it is free: no paid plan and no card behind it. Its limits are the five numbers below. The traffic limit adds up both directions; the month turns on the 1st, in UTC. When a limit is reached, visitors get a 429 page naming the limit and saying when it ends.
- 2 tunnels open at the same time; a reserved name does not count
- 1000 requests per minute, in a sliding window of 60 seconds
- 25 GiB of traffic per month, both directions
- $0 a month, with no card asked
- 429 past a limit, with a
Retry-Afterheader
One command, one public URL
localhostly 3000 opens one outgoing WebSocket connection from your computer to the localhostly server and keeps it; nothing is opened on your router or firewall. When a visitor opens your URL, the server reads the name in the address, sends the request down that connection, and the agent replays it on your local port and streams the answer back. HTTP and WebSocket both pass.
- Get a token. Enter your e-mail in the sign-up form. The token shows up on the sign-up page; no confirmation e-mail is sent.
- Save it.
localhostly login <token>keeps it in~/.localhostly/config.json, readable only by your user. - Open the tunnel.
localhostly 3000. The terminal prints the public URL and then one line per request: time, method, path, status and milliseconds.
Your app receives the visitor's method, path, headers and body, with X-Forwarded-For, X-Forwarded-Proto: https and X-Forwarded-Host. Add --host myapp.test:3000 to send it another address and Host. The name is 3 English words drawn at random, adjective, color and animal, and nobody chooses it.
The tunnel stays open as long as the agent answers pings
The tunnel stays open as long as the agent answers the server's pings: one every 30 seconds, and the connection is closed after 60 seconds without an answer. In a measured run, a tunnel idle for 620 seconds did not drop.
If the connection does drop, the agent reconnects on its own, waiting 1 second and doubling up to 60 seconds, and gets the same name back for 10 minutes after the drop; after a longer drop it comes back with a new name and prints name changed. To keep it up across reboots, install it as a system service, below.
Webhooks and API clients skip the warning page
A browser sees a warning page on its first visit, saying the site runs on someone's own computer; the button on it sets a cookie for 24 hours. A webhook call, a curl call or any API client is not a browser and gets no warning page: the request reaches your app directly, and any other client skips it with the X-Localhostly-Skip-Warning header.
Paste https://<name>.localhostly.iblabs.io/<your path> into the provider's webhook settings, and each call shows in your terminal with method, path, status and time.
Streaming responses and big uploads pass through
A request body travels in pieces of up to 64 KiB, in both directions, and is never held whole in memory. The measured run sent a 20 MiB upload with peak memory under 100 MiB and delivered a 20 MiB download to a slow visitor the same way.
Server-Sent Events reach the visitor as your app writes them: in a measured stream, events written at 0, 3 and 6 seconds arrived at 0.6, 3.6 and 6.6 seconds. Your app has 60 seconds to send the first byte; after that there is no time limit, so long downloads pass. A visitor's WebSocket reaches your app, and the upgrade completes only once your app accepts it, with the subprotocol it chose.
Lock the tunnel with a password or an IP list
With --password every visitor must enter a password in the browser's login box; without a value, localhostly generates one of 12 characters and shows it only in your terminal. Your app never sees the visitor's Authorization header.
With --allow 203.0.113.7,10.0.0.0/8 only those IP addresses or ranges get in, IPv4 or IPv6, up to 64 entries; any other address gets a 403 page that shows it, and its WebSocket upgrade gets 403 too. A tunnel with --allow shows no warning page, and with --password as well a visitor needs both.
- The server's log keeps method, path without the query string, status, time, tunnel name and account, never a request body or a cookie.
- On our side the token exists only as its SHA-256 hash.
- HTTPS ends at the localhostly load balancer: the localhostly server sees and relays your traffic, so this is not end-to-end encryption.
Keep it running as a system service
localhostly service install 3000 installs the tunnel as a system service: systemd on Linux, launchd on macOS, a Windows service. It reconnects by itself, remembers its name and asks for it again after a reboot, and keeps trying if the server refuses it. localhostly service status reports whether it is running; 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
Then save your token with localhostly login <token>, and localhostly 3000 opens the tunnel.
Get your token
Sign-up asks for an e-mail address and nothing else, and there is no plan to pick: the token appears on the sign-up page, with no confirmation e-mail, and localhostly login <token> saves it on your computer.