Skip to content
PortBay

Local development

Local HTTPS with .test domains on macOS: mkcert, dnsmasq, and the one-click way

The full manual setup — mkcert for a trusted CA, dnsmasq for wildcard .test DNS, a proxy to serve it — and the one-click version that issues and renews everything for you.

Nour Beiruti8 min read

To get trusted HTTPS on a local .test domain on macOS you need three things: a certificate authority your system trusts (mkcert creates one), DNS that resolves *.test to your machine (dnsmasq, or an /etc/hosts line per site), and a web server that serves your project on that domain with the issued certificate. This guide walks the manual setup first, then shows the one-click version.

Why bother — localhost:3000 mostly works, until it doesn't

Plain http://localhost:3000 breaks exactly the features you most need to test: Secure cookiesaren't sent over http, so session and auth flows behave differently than production. OAuth providers increasingly require https redirect URIs. Service workers, parts of the web crypto API and mixed-content rules are https-gated. Subdomains(tenant1.yourapp.test) can't be expressed on localhost at all. And a self-signed certificate without a trusted CA buries you in browser warnings that train you to click through — the worst possible habit.

One naming note: use .test, not .dev or .local. The .test TLD is reserved for exactly this purpose (RFC 2606). .devis a real Google- owned TLD on the HSTS preload list — browsers force-redirect it to https against real certificates you can't issue. .local collides with mDNS/Bonjour on macOS.

Step 1: a locally trusted CA with mkcert

mkcertcreates a local certificate authority, installs it into the macOS system trust store (and Firefox's, with nss), and issues certificates that every local browser accepts:

brew install mkcert nss
mkcert -install

# issue a cert for your site (wildcards work)
mkcert yourapp.test "*.yourapp.test"

That drops yourapp.test+1.pem and yourapp.test+1-key.pemin the current directory. Two things to know: certificates are only trusted on machines that have your CA installed (that's the point — never share the CA key), and mkcert certs expire after about two years and won't renew themselves.

Step 2: resolve .test to your machine

The quick way is one hosts line per site:

sudo sh -c 'echo "127.0.0.1 yourapp.test" >> /etc/hosts'

That stops scaling the moment you want wildcard subdomains — /etc/hostsdoesn't do wildcards. The durable setup is dnsmasq, answering 127.0.0.1 for anything under .test:

brew install dnsmasq
echo 'address=/.test/127.0.0.1' >> $(brew --prefix)/etc/dnsmasq.conf
sudo brew services start dnsmasq

# tell macOS to send .test queries to dnsmasq
sudo mkdir -p /etc/resolver
sudo sh -c 'echo "nameserver 127.0.0.1" > /etc/resolver/test'

Step 3: serve the project on that domain

Finally, a reverse proxy (nginx, Caddy) terminates TLS with the mkcert certificate and forwards to your app's real port. A minimal Caddyfile:

yourapp.test {
  tls yourapp.test+1.pem yourapp.test+1-key.pem
  reverse_proxy localhost:3000
}

Multiply this by every project, remember which port each app runs on, and re-issue certificates when they expire — and you've built yourself a small system-administration job. It all works; it's just maintenance that has nothing to do with the thing you're building.

The one-click version

This entire stack is what PortBay automates. Add a project folder and press play: PortBay detects the framework, provisions the runtime, issues and renews the mkcert certificate, registers the .testdomain and serves the site over trusted HTTPS — no hosts edits, no dnsmasq config, no proxy files. Every project gets a real https URL the moment it starts, plus the rest of the local stack (per-project MySQL/Postgres, email capture, one-click Cloudflare tunnels) when you need it. It's free and open source, and if you're coming from a tool that already does part of this, the comparisons cover the differences honestly: vs Valet, vs Laravel Herd, vs MAMP.

Why this matters more now: your agents browse over HTTPS

A trusted local URL stopped being a nicety when AI coding agents started verifying their own work. An agent that builds a checkout flow needs to load the page; OAuth callbacks, Secure cookies and service workers need to behave as they will in production while it does. A real https://yourapp.test gives Claude Code or Codex the same environment your users get — the environment layer is the difference between an agent that reports done and an agent that proved it.

Troubleshooting the manual setup

  • Browser still warns: re-run mkcert -install; for Firefox, confirm nss is installed. Restart the browser — trust stores are read at launch.
  • .test doesn't resolve: scutil --dns | grep test should list the resolver; dig yourapp.test @127.0.0.1should answer 127.0.0.1. If the first works and the second doesn't, dnsmasq isn't running.
  • Cert expired after ~2 years:mkcert doesn't auto-renew. Re-issue and update the proxy paths (or use a tool that renews for you).
  • VPN/corporate DNS overrides the resolver: some VPN clients rewrite DNS order. The /etc/resolver/test approach usually survives; per-app DNS proxies sometimes don't.
PortBay mascot — a friendly blue tugboat

Run your first local site in one click.

Download for macOS

Free & open source · macOS 11+ on Apple Silicon · Pro from $10/mo