Localhost Routing on macOS
When you have several local projects running on different ports, URLs like this get annoying quickly:
http://localhost:5173
http://localhost:5990
http://localhost:8787
A simple local Caddy reverse proxy lets you replace them with memorable URLs:
http://my-app.localhost
http://admin.localhost
http://docs.localhost
For this use case, Caddy is a great fit: the configuration is tiny, WebSockets generally work without extra setup, and Homebrew can run it automatically as a macOS service.
Install Caddy
If you already have Homebrew:
brew install caddy
The Homebrew Caddyfile lives at:
$(brew --prefix)/etc/Caddyfile
You can print the exact path with:
echo "$(brew --prefix)/etc/Caddyfile"
Add a local route
Suppose your dev server runs at:
http://localhost:5173
Add this to the Caddyfile:
http://my-app.localhost {
reverse_proxy 127.0.0.1:5173
}
Now visit:
http://my-app.localhost
Your application still runs on port 5173; Caddy simply provides a friendlier address in front of it.
You normally do not need to edit /etc/hosts. Names under .localhost are intended for local loopback use.
Add more projects
Just add another block for each application:
http://my-app.localhost {
reverse_proxy 127.0.0.1:5173
}
http://admin.localhost {
reverse_proxy 127.0.0.1:5174
}
http://docs.localhost {
reverse_proxy 127.0.0.1:8787
}
Caddy’s reverse proxy handles WebSockets, so development features such as Vite HMR usually work without additional configuration.
TIP: Have each project run on a specific port by adding it to the appropriate script in the package.json file: vite dev --port 5999
Run Caddy automatically
Start Caddy through Homebrew:
brew services start caddy
This starts it immediately and registers it as a macOS service that runs when you log in.
Useful commands:
brew services list
brew services restart caddy
brew services stop caddy
There is generally no reason to create your own launchd configuration when using the Homebrew package.
Reload after editing the Caddyfile
Validate the configuration first:
caddy validate --config "$(brew --prefix)/etc/Caddyfile"
Then reload it:
caddy reload --config "$(brew --prefix)/etc/Caddyfile"
Or, for a development machine, simply restart the service:
brew services restart caddy
Optional: one file per project
If you accumulate a lot of routes, you can split them out into separate files. The main Caddyfile becomes simply:
import caddy-sites/*.caddy
Create the directory:
mkdir -p "$(brew --prefix)/etc/caddy-sites"
Then give each project its own file, for example:
$(brew --prefix)/etc/caddy-sites/my-app.caddy
containing:
http://my-app.localhost {
reverse_proxy 127.0.0.1:5173
}
This makes adding and removing new routes easy. You could probably whip up a simple shell script to add a new one in a single step.
HTTP is usually enough
For normal local development, I prefer explicitly using HTTP:
http://my-app.localhost {
reverse_proxy 127.0.0.1:5173
}
It avoids local certificate management and is enough for most applications.
If you specifically need to test TLS, secure-cookie behavior, HTTPS-only APIs, or similar production behavior, remove the http:// prefix:
my-app.localhost {
reverse_proxy 127.0.0.1:5173
}
Caddy can then use its local certificate authority for HTTPS.
Troubleshooting
First make sure the application works directly:
http://localhost:5173
Then check Caddy:
brew services list
caddy validate --config "$(brew --prefix)/etc/Caddyfile"
If the application works on localhost but not through the friendly hostname, the development server may restrict allowed hostnames. Look for settings named things like allowedHosts, trustedHosts, or host.
If Caddy says port 80 is already in use:
lsof -nP -iTCP:80 -sTCP:LISTEN
That will show what is already listening there.
That’s the whole setup: keep your existing dev servers on their normal ports and let Caddy provide memorable .localhost names in front of them.