Give every project a stable .localhost URL so you stop remembering ports
By Flavio
Give each Astro or Vite project a stable https://name.localhost address with Local Hoster, so the port behind it can stay out of your bookmarks.
The shop was on port 4321 yesterday. Today something else took 4321, so Astro moved, and the admin is on 5173 or 5174 depending on what you already started. You type the wrong port, the bookmark is stale, and an OAuth callback you saved last week points at a server that is no longer there.
A custom .localhost name stays put. https://shop.localhost is the shop. https://admin.localhost is the admin. You stop remembering which number belonged to which project.
Local Hoster does that per project. You set a localhost name in package.json, put localhoster in front of the dev script, and run npm run dev. It picks a port from the name, registers the name with Caddy, and starts your server. The walkthrough for a hand-written Caddyfile, and for trusting HTTPS the first time in more detail, is HTTPS on localhost with Caddy. Here the job is two projects at once, and what the port and the name are doing.
What you need
Local Hoster runs on macOS or another Unix-like system. You need Node.js 18 or newer, npm, and Caddy. The dev command has to accept a --port option. The app page says that covers Astro, Vite, and other servers that take the same flag.
After you buy it, download the source zip from your Polar receipt and unzip it. The receipt copy is the latest one when you download it again later. In the unzipped folder:
npm link
That puts localhoster on your PATH. There are no runtime npm dependencies. The command is the check that the link worked:
localhoster --help
The docs tell you to run that and don’t include the help text. The shape of the output is a usage screen. “command not found” means the link missed your PATH.
Caddy has to be running with its admin API at http://localhost:2019. On a Mac with Homebrew:
brew install caddy
A new Homebrew install has no Caddyfile yet. The docs create an empty one and start the service:
touch "$(brew --prefix)/etc/Caddyfile"
brew services start caddy
If you already run Caddy, read its config before you point Local Hoster at it. Local Hoster adds and removes its own tagged routes through the admin API. Keep that API on your machine.
Trust Caddy’s local certificate authority once:
caddy trust
That changes local certificate trust and may ask for an administrator password. It is for Caddy’s certificates. The .localhost addresses are only for development on this computer. Nothing here is a public site, and you don’t edit /etc/hosts.
The full setup notes are on the Local Hoster docs. On Linux, install Caddy from your packages or from Caddy’s install page, then use the same admin API and caddy trust.
Name the shop and wrap its dev script
The name is what you type in the browser. The port is what Caddy uses behind that name. If you skip the name, Local Hoster falls back to the package name. That’s awkward for a scoped package, because a hostname label is letters, digits, and hyphens, and the docs lowercase it and add .localhost. A field you choose avoids that. Dots can separate labels. A short name such as shop or shop-preview is what the docs suggest.
Example 1: an Astro shop at shop.localhost
The Local Hoster page shows this with a fictional project, ceramics-shop. The screenshot’s package.json includes a localhost name and a wrapped dev script. Merge those fields into the file you already have. Leave the rest of the project’s settings alone.
{
"name": "ceramics-shop",
"localhost": "shop",
"scripts": {
"dev": "localhoster astro dev"
}
}
localhoster has to come first. It reads the name, talks to Caddy, then starts the command you wrote after it. The app page says that command is started directly, not through a shell, so the script stays a program plus its arguments. astro dev is that program. Astro accepts --port, which is the flag Local Hoster will add.
From the shop folder, start it the way you always do:
npm run dev
You do this after Caddy is up. If Caddy isn’t reachable, Local Hoster has nowhere to register shop.localhost, and the name won’t open. The dev server might still start. The stable URL is the part that failed.
Read the two routes it prints
Local Hoster maps the hostname to a port between 10000 and 59999. The same hostname maps to the same port the next time you start it. That mapping does not reserve the port. Another process can still be listening there.
It then appends --port and that number to your command. The terminal on the app page shows the fictional shop doing this. These two lines are the Local Hoster part of that screenshot:
http://shop.localhost -> localhost:15515
https://shop.localhost -> localhost:15515
15515 sits inside the documented range. The screenshot’s Astro line under that is Local http://localhost:15515/, which is the dev server accepting the port it was given. Open https://shop.localhost. The HTTP address works too. The docs don’t describe a redirect from one to the other. Use HTTPS when the feature you are testing wants a secure origin. The app page lists secure cookies, service workers, WebAuthn, credential APIs, and OAuth callback URLs as the reasons.
I use this for flaviocopes.com as well. The app page says the project’s localhost name is what I open, and the address stays when the project restarts. Browser history starts matching the project name instead of a port that moved.
If your server uses a different flag than --port, the docs say to adapt the command or the source before you wrap it. Local Hoster will still append --port. A server that ignores unknown flags can look healthy in the terminal and answer on its own default port, while Caddy sends traffic at the port in those two lines.
Put the name in the callback, not the port
The number in those two lines is for Caddy and the dev server. The origin you hand to a browser, a cookie, or an OAuth app is the name. Register http://localhost:15515 because the shop screenshot printed it, and the registration is stuck on a port. https://shop.localhost is the origin you open. 15515 is the mapping that screenshot shows for shop. The docs say the same hostname keeps mapping to the same port. You still bookmark the name, because a different project name would be a different port, and the URL you type should not be the thing that changes.
Secure cookies, service workers, WebAuthn, credential APIs, and OAuth callbacks are the list on the app page. Set the shop’s callback to https://shop.localhost plus the path your app already uses. The admin gets https://admin.localhost, not the shop’s origin and not a port. Two projects are two origins. A cookie set on one is not sent to the other. That is what you want when the shop session and the admin session should stay apart.
HTTP answers too, because both routes are registered. Use the HTTP URL when a client you are debugging cannot speak TLS yet. Point the callback, and any secure cookie, back at the HTTPS origin before you call the flow done. The other tutorial’s hand-written Caddyfile redirects HTTP to HTTPS, while the two lines Local Hoster prints are both live routes to the same port.
Start the shop again after Control-C and the same hostname prints the same kind of lines again. The routes were removed when the process stopped, then added on the next npm run dev. You don’t get a second shop address unless you change the localhost field. Git keeps that field because it lives in your package.json. Caddy’s certificate authority stays with the Caddy install, not with the repo.
Give the admin its own name
A second project needs its own hostname. The port comes from that hostname, so the shop and the admin don’t share a number, and they don’t share a bookmark. You can leave the shop running. Stopping one project removes that project’s routes. The docs describe the cleanup as the HTTP route, the HTTPS route, and the TLS automation policy for the child that exited.
Example 2: a Vite admin at admin.localhost
The app page’s video is an Astro shop plus a Vite admin, and one screenshot is an admin dashboard at admin.localhost. This package.json is an example in that shape, not a second screenshot. Vite’s dev command accepts --port, which is the requirement on the app page.
{
"localhost": "admin",
"scripts": {
"dev": "localhoster vite"
}
}
Start it from the admin folder, in a second terminal:
npm run dev
The shape of the output is the same pair of lines as the shop, with admin.localhost and the port that name maps to. The docs don’t print that second number. Open https://admin.localhost and leave https://shop.localhost open next to it. Two names, two processes, one Caddy.
Names are lowercased. Admin and admin are the same hostname, and so the same port. If you need shop.localhost and shop-preview.localhost, those are different labels, and each gets its own mapping. A scoped package name is a bad label. Set localhost yourself.
Trust HTTPS, then stop one project
caddy trust is once per Caddy install, not once per project. After it succeeds, Local Hoster asks Caddy for the temporary TLS config of each project you start. The certificates stay on your machine. If the browser rejects the certificate, the docs say to check that caddy trust succeeded for the Caddy that is issuing it. A second Caddy, or an admin API you pointed elsewhere, can be a different authority.
The default admin API is http://localhost:2019. An owner-controlled replacement can be set with LOCALHOSTER_CADDY_ADMIN. The docs don’t print a sample value. Use it when your Caddy admin endpoint is not the default, and keep it local.
When you are done with the admin, press Control-C in that terminal. Local Hoster removes its HTTP route, its HTTPS route, and its TLS automation policy when the child exits or when it handles a stop signal. The shop’s terminal is a different process. Leave it running if you still want https://shop.localhost.
Start the admin again with npm run dev. The same hostname maps to the same port, and the routes are registered again. The name in package.json is yours, so Git keeps it. Caddy’s certificate authority belongs to the Caddy install, not to the repo. Local Hoster does not upload the project and it does not open a public tunnel.
To drop the wrapper, take localhoster off the script and remove the localhost field. That does not undo caddy trust, and it does not uninstall Caddy. A running project’s routes go away when that process stops. Quitting Caddy takes the proxy down for every name at once.
If something looks wrong
Example 3: the port from the hostname is already taken
The mapping is deterministic, and it does not reserve the port. The docs’ symptom is a port conflict from the server. The shop’s screenshot used 15515 for shop. If something else is already listening there, the dev server reports the conflict. Find that listener before you stop it, or pick another hostname so Local Hoster maps a different port. Find what’s using a port is the inspection step. The docs call this a port conflict and don’t quote the server’s line. Read the message your dev server prints.
If localhoster is not found, npm link didn’t land on the PATH you are using. Open a new terminal in the unzipped source folder and run it again.
If the name doesn’t resolve, leave /etc/hosts alone. .localhost is already local. Check that Caddy is running and that http://localhost:2019 answers. If you moved the admin API, set LOCALHOSTER_CADDY_ADMIN to that endpoint.
If the browser warns about the certificate, run caddy trust again and approve the prompt. The authority has to be the one your running Caddy is using.
If https://shop.localhost shows an old project, look at the two route lines in the terminal you just started. They are the port Caddy is using now. A server that ignored --port can still be serving its own default port, which those lines are not.
If Control-C leaves the name pointing at a dead port, the child may not have exited, or Caddy’s admin API was unreachable during cleanup. Stop the leftover process, confirm Caddy’s admin API, and start npm run dev again so the routes are written fresh.
If you wrapped a server that doesn’t take --port, it can start and still be unreachable at the .localhost name. The docs’ fix is to adapt that command, or the source, before you put localhoster in front of it.
Get Local Hoster
Give your development projects memorable .localhost URLs with local HTTPS through Caddy. Includes the localhoster CLI and full MIT source.
See Local Hoster → · Read the docs · $20 including tax, with full source code.
Want me to talk about your product? You can sponsor this site.