localhostly is a Cloudflare Tunnel alternative with no DNS setup
If you are leaving Cloudflare Tunnel, here is what you get instead: one e-mail for the account and one command for a public HTTPS URL, with no domain to point at it. Run localhostly 3000 and port 3000 answers at https://brave-amber-fox.localhostly.iblabs.io, behind NAT. The account comes with 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 account in numbers
Five numbers bound every account, listed below; traffic adds up both directions, and the month starts over on the 1st, in UTC. Past a limit the visitor sees a 429 page that names the limit and the time it ends.
- 2 tunnels open at once; a name held in reserve does not count
- 1000 requests a minute, in a sliding window of 60 seconds
- 25 GiB a month of traffic, both directions
- 1 e-mail address, the whole sign-up
- 429 past a limit, with
Retry-After
One command, one public URL
The agent on your computer opens one outgoing WebSocket connection to the localhostly server and holds it; your router and firewall stay as they are. A visitor opens the URL, the server reads the name out of the address, sends the request down that connection, and the agent repeats it on your local port and streams the answer back the same way. Several requests can be in flight on that one connection at the same time, and HTTP and WebSocket both pass.
- Get a token. Enter your e-mail in the sign-up form. The token appears on the page at once; there is no confirmation e-mail.
- Save it.
localhostly login <token>writes it to~/.localhostly/config.json, readable only by your user. - Open the tunnel.
localhostly 3000prints the public URL first, then a line for each request with its 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 on top; --host myapp.test:3000 sends it another address and Host. The name is 3 English words, adjective, color and animal, drawn at random by the server.
The URL is ready in the same command, with no DNS to set up
There is no domain to bring and no record to create: the server draws the name of your tunnel when the agent connects, and the address comes with the HTTPS certificate of *.localhostly.iblabs.io already on it. The account is an e-mail address: the token is on the sign-up page the moment you submit the form, and localhostly login <token> is the whole setup. From there, localhostly 3000 prints the URL and the first visitor can open it.
The name stays with the agent. After a drop it reconnects by itself and gets the same name back for 10 minutes, so a URL you pasted somewhere keeps working; installed as a system service, it asks for the name again after a reboot.
Webhooks reach your app without a warning page
A browser's first visit gets a warning page saying the site is served from someone's own computer, and the button on it sets a 24-hour cookie. Webhook deliveries and API clients are not browsers and get no such page: the request lands on your app as it was sent, and any client can skip the page with the X-Localhostly-Skip-Warning header.
Give the provider https://<name>.localhostly.iblabs.io/<your path>; each delivery shows up as a line in your terminal with method, path, status and time.
Server-Sent Events, long downloads and big uploads pass through
A response that your app writes over time arrives at the visitor as it is written: in a measured stream, Server-Sent Events written at 0, 3 and 6 seconds reached the visitor at 0.6, 3.6 and 6.6 seconds. The 60-second limit covers the first byte only; once the response has started there is no limit, so a long download or a stream that stays open passes.
Request and response bodies cross the tunnel in pieces of up to 64 KiB, never whole in memory: a 20 MiB upload went through with peak memory under 100 MiB in the measured run, and a 20 MiB download reached a slow visitor the same way. A visitor's WebSocket reaches your app, and the upgrade completes only when your app accepts it, with the subprotocol it chose.
Lock the tunnel with a password or an IP list
With --password the browser asks every visitor for a password before anything loads; without a value, localhostly generates one of 12 characters and shows it in your terminal and nowhere else. The visitor's Authorization header stops at the server and never reaches your app.
With --allow 203.0.113.7,10.0.0.0/8 the tunnel admits only those IP addresses or ranges, IPv4 or IPv6, up to 64 entries; every other address gets a 403 page that shows the address, and a WebSocket upgrade from it gets 403 too. With --allow there is no warning page, and with --password as well the visitor needs both.
- The server logs method, path without the query string, status, time, tunnel name and account for each visit, and never a request body or a cookie.
- Your token is kept on our side only as a SHA-256 hash.
- HTTPS terminates at the localhostly load balancer, so the localhostly server sees and relays your traffic; this is not end-to-end encryption.
Keep it running as a system service
localhostly service install 3000 registers the tunnel as a system service, systemd on Linux, launchd on macOS or a Windows service, that reconnects by itself, remembers its name, asks for it again after a reboot and keeps trying when the server refuses it. localhostly service status reports the state; localhostly service uninstall takes it out.
Install
localhostly runs on Node.js 24 or newer.
-
Try it without installing:
npx localhostly 3000 -
Install it once:
npm i -g localhostly
The package on npm holds the agent only. Save your token with localhostly login <token> and open the tunnel with localhostly 3000.
Get your token
An e-mail address is all the sign-up asks: the token shows on the sign-up page, no confirmation e-mail follows, and localhostly login <token> stores it on your computer.