Guide
How to create .test domains on Mac.
http://localhost:8000 works until you have three projects, cookies bleeding between them and an app that needs subdomains. Giving each project a name like shop.test fixes that. This guide covers the two ways to do it on macOS and why the TLD matters. At the end I show how Bothy, my Mac app for local PHP development, sets it up.
For a site or two, add lines to /etc/hosts. For every *.test name at once, run dnsmasq with address=/.test/127.0.0.1 and create /etc/resolver/test containing nameserver 127.0.0.1. Use .test because it's reserved for this. .local belongs to Bonjour, and .dev is a real, HTTPS-only TLD.
Why .test, not .local or .dev
The top-level domain isn't cosmetic. Pick the wrong one and you get slow lookups or a browser that refuses to connect.
.testis reserved for testing. RFC 2606 (1999) set aside.test,.example,.invalidand.localhostso they'd never be real TLDs. RFC 6761 later listed them as special-use names. No one can registershop.test, so it will never collide with a real site..localis for multicast DNS. macOS sends.locallookups to Bonjour/mDNS (RFC 6762), the system that finds printers and other Macs on your network. Using it for dev sites causes slow, intermittent lookups and odd delays of several seconds..devis a real TLD. Google owns it and it's on the browsers' HSTS preload list, so every browser forces HTTPS for any.devname. The same applies to.app. Many older setups broke when this happened..localhostis also reserved, and browsers resolve*.localhostto your machine by themselves. Command-line tools and PHP's own HTTP requests may not, so it's less predictable than.testwith a real resolver.
Laravel Valet, Laravel Herd and Bothy all default to .test for these reasons.
Method 1: /etc/hosts (one name at a time)
The hosts file maps names to addresses before DNS is consulted. It's built in and needs nothing installed.
- 1Edit the hosts file
Add an IPv4 line and an IPv6 line for each name. Including
::1avoids a delay when something tries IPv6 first.sudo nano /etc/hosts 127.0.0.1 shop.test ::1 shop.test 127.0.0.1 api.shop.test ::1 api.shop.test
- 2Flush the DNS cache
sudo dscacheutil -flushcache sudo killall -HUP mDNSResponder
- 3Check it resolves
ping -c1 shop.test
The limits of /etc/hosts
It's fine for a couple of sites, but it doesn't scale:
- No wildcards.
*.testisn't valid in a hosts file. Every site and every subdomain needs its own lines, including multisite subdomains and tenant subdomains. - It needs sudo each time, so tools can't add sites without asking for your password.
- It's easy to forget. Old entries pile up, and an entry pointing a real domain at 127.0.0.1 can confuse you for hours.
A local DNS server fixes all of this.
Method 2: dnsmasq and /etc/resolver (every *.test name)
dnsmasq is a small DNS server. You tell it to answer every .test query with 127.0.0.1. Then macOS's per-domain resolver files send only .test lookups to it, and everything else uses your normal DNS. This is how Valet and Bothy do it.
- 1Install dnsmasq
brew install dnsmasq
- 2Answer every .test name with 127.0.0.1
Add these lines to dnsmasq's config. The leading dot matches
testand everything under it, however many levels deep.# $(brew --prefix)/etc/dnsmasq.conf address=/.test/127.0.0.1 listen-address=127.0.0.1
- 3Start dnsmasq as root
DNS uses port 53, a privileged port, so the service runs under
sudoas a system daemon.sudo brew services start dnsmasq
- 4Send .test lookups to it
Any file in
/etc/resolver/named after a domain sends that domain's queries to the listed nameserver.sudo mkdir -p /etc/resolver echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/test
- 5Check macOS picked it up
scutil --dnsshould list a resolver for domaintest. Then any name works with no further setup.scutil --dns | grep -A2 test ping -c1 anything.test ping -c1 deep.sub.anything.test
Serving the site
DNS only gets the browser to 127.0.0.1. Your web server still needs to know which folder shop.test maps to. In Caddy that's a site block named shop.test, and in nginx it's server_name shop.test. Set up a vhost for each site, or a wildcard vhost that maps the subdomain to a folder, which is what Valet does. For the certificate side, see local HTTPS on macOS.
Pitfalls and troubleshooting
dig shop.testsays NXDOMAIN, but the browser works.digandnslookuptalk to DNS servers directly and ignore/etc/resolver. Test withdig @127.0.0.1 shop.test,ping, ordscacheutil -q host -a name shop.test.- Nothing resolves after a reboot. dnsmasq wasn't started as root, or its config has an error. Check
sudo brew services list, and rundnsmasq --testto validate the config. - It stops working on a VPN. Some VPN clients take over DNS completely and ignore resolver files. Try disconnecting, or look for a split-DNS setting in the VPN client.
- Port 53 is already in use. Another tool's DNS server is running. Valet, Herd and Bothy all run one, so only have one active. Find the owner with
sudo lsof -i :53. - The browser searches instead of loading. Type the full
http://orhttps://the first time. Some browsers don't recognise.testas a domain until you've visited one. - Other devices can't see your sites. That's expected. The resolver is on your Mac only. To show a site to a phone or a client, use a tunnel.
.test domains with Bothy
Bothy sets up Method 2 during first launch, the one time it asks for your password. dnsmasq answers *.test on 127.0.0.1:53, /etc/resolver/test points macOS at it, and Caddy serves each site over HTTPS. Adding a site is then one step, and subdomains are just names with a dot (app.client becomes app.client.test):
bothy site add app.client --php 8.3 --db
bothy site rename shop store # now https://store.test
If a name stops resolving, the troubleshooting docs start with bothy doctor, which checks the resolver file and whether dnsmasq is running. bothy status shows the services. For a public URL, Share opens a temporary Cloudflare quick tunnel. Uninstalling removes /etc/resolver/test along with the rest of Bothy's setup.
Questions
No. The hosts file only matches exact names. For *.test, use dnsmasq with an /etc/resolver/test file.
macOS resolves .local through Bonjour/mDNS, which leads to slow or flaky lookups. .test is reserved for exactly this use.
.dev became a real Google-owned TLD on the HSTS preload list, so browsers force HTTPS on every .dev name. Switch to .test.
No. It only applies to names ending in .test. All other lookups use your normal DNS.
Delete /etc/resolver/test, run sudo brew services stop dnsmasq, and optionally brew uninstall dnsmasq.
Doing it with Bothy: the dnsmasq and /etc/resolver/test setup happens once at first launch. After that, every site you add, subdomains included, resolves and loads at https://name.test. See what setup installs.