Better local development with .localhost subdomains
If you run a few local services at once, frontend, API, maybe auth, you probably use localhost with different ports. It works until cookies, CORS, or HTTPS show up and start arguing with each other.
.localhost subdomains take a lot of that pain out.
How .localhost works
Any name under *.localhost resolves to 127.0.0.1 on macOS, Linux, and Windows. You do not edit hosts files for that.
Clean multi-service dev environments
Instead of remembering which port is which service, I name them:
web.localhost:3000-> frontendapi.localhost:3001-> backendauth.localhost:5000-> auth or mock SSOpayments.localhost:5002-> webhook listener
All of those land on 127.0.0.1. Route them through Vite, Webpack, Traefik, or keep the ports hard-coded. The URLs start looking closer to production, which makes config copy-paste less of a lie.
Cookie behavior on localhost
Ports do not separate cookies
Browsers ignore the port when they scope cookies. A cookie set on localhost:3000 also goes to localhost:3001. In production your services usually sit on separate hosts or subdomains with their own cookie space. On plain localhost, they share one jar.
That leads to cookies leaking across unrelated services, and it makes Domain, Path, and SameSite tests messy.
127.0.0.1 and localhost are different origins
They both point at your machine. Browsers still treat them as different sites. A cookie on localhost:3000 will not ride along to 127.0.0.1:3001.
Using .localhost subdomains
With .localhost names, each service gets its own host:
- cookies stay separated by default
- shared cookies work when you set
Domain=.localhost SameSiteandSecurebehave closer to production- CORS looks like a multi-host setup instead of one host with many ports
Other small wins: localStorage and sessionStorage split by subdomain, SameSite=None can work once you add HTTPS with mkcert, and the URLs look like the real deployment shape.
Reserved name, local only
RFC 6761 reserves .localhost. Resolvers map it to 127.0.0.1 or ::1 without asking the public internet.
That holds on the big three:
- macOS built-in DNS
- Linux with systemd-resolved and friends
- Windows DNS
Practical effects I care about: the name never triggers an outbound DNS lookup, it cannot leak when a VPN or custom resolver is being weird, and nobody can register something.localhost as a real public domain.
TLS with mkcert
Some cookie flags need HTTPS. mkcert issues trusted certs for local names:
# Install mkcert
brew install mkcert # macOS
# or
sudo apt install mkcert # Ubuntu/Debian
# or
choco install mkcert # Windows (Chocolatey)
# Install the local CA
mkcert -install
# Generate certificates for your domains
mkcert web.localhost api.localhost auth.localhost
After that you get https://web.localhost, https://api.localhost, and so on with a browser that trusts them.
Proxies, containers, and dev tools
.localhost drops into setups I already have:
- route
web.localhostto one container andapi.localhostto another - reverse proxies that key off the Host header
- Vite, Next.js, and similar tools that bind on a hostname
- Cypress and friends that already treat
.localhostas local
Vite
// vite.config.js
export default {
server: {
host: 'web.localhost',
port: 3000,
https: true
}
}
Docker Compose
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
extra_hosts:
- "web.localhost:127.0.0.1"
- "api.localhost:127.0.0.1"
Environment variables
// config.js
const isDev = process.env.NODE_ENV === 'development';
export const config = {
apiUrl: isDev ? 'https://api.localhost' : 'https://api.production.com',
authUrl: isDev ? 'https://auth.localhost' : 'https://auth.production.com',
webUrl: isDev ? 'https://web.localhost' : 'https://app.production.com'
};
When something breaks
Stale DNS cache
If a subdomain refuses to resolve, flush local DNS:
# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Linux
sudo systemctl restart systemd-resolved
# Windows
ipconfig /flushdns
Port conflicts
# Check what's running on port 3000
lsof -i :3000
SSL certificate issues
# Reinstall mkcert CA
mkcert -install
# Regenerate certificates
mkcert -key-file key.pem -cert-file cert.pem web.localhost api.localhost
Closing
I reach for .localhost whenever local multi-service work starts looking like a tiny production topology. Named hosts, sane cookies, local TLS with mkcert, and less surprise when the same config moves closer to prod.