Back to blog
#macOS #DNS #DevTools #Networking

Custom Local Domains on macOS: Why /etc/resolver Files Are Not Enough

When building a hobby project that needed multiple local services under a single domain (like api.myapp.test, auth.myapp.test), I wanted them to resolve to 127.0.0.1 without editing /etc/hosts for every new subdomain.

The natural approach everyone suggests is creating a file in /etc/resolver/. I did that, and it seemed to work. Until it did not.

The /etc/resolver/ Approach and Why It Falls Short

The classic recipe is:

sudo bash -c 'echo "nameserver 127.0.0.1\nport 1053" > /etc/resolver/myapp.test'

This tells macOS to route DNS queries for *.myapp.test to 127.0.0.1:1053. Running scutil --dns shows the resolver was registered. But in practice, applications would still query the wrong nameserver for my custom domain.

After digging through Apple developer forums, I found a response from Quinn “The Eskimo!” on the Apple Developer Technical Support team that explained the issue clearly:

“macOS does support scoped DNS, but the approach you’re using won’t work. On Apple platforms the classic resolver.conf infrastructure is a compatibility measure, not the source of truth. You can’t use it to configure scoped DNS.”

The /etc/resolver/ directory is a legacy compatibility mechanism. It is read in some contexts but is not the canonical way to configure per-domain DNS routing on modern macOS.

The Correct Approach: scutil

The right way to configure scoped DNS on macOS is through scutil, which writes directly into the System Configuration framework that the OS actually uses.

sudo scutil
d.init
d.add ServerAddresses * 127.0.0.1
d.add SupplementalMatchDomains * myapp.test
d.add ServerPort 1053
set State:/Network/Service/myapp-dev/DNS
quit

This creates a supplemental resolver entry that routes all myapp.test queries to 127.0.0.1:1053. You can verify it took effect:

scutil --dns

Look for a new resolver entry with Supplemental in the flags section.

To remove the resolver when you no longer need it:

sudo scutil
remove State:/Network/Service/myapp-dev/DNS
quit

The service name (myapp-dev) just needs to be unique. You can use anything that does not conflict with existing services.

Running a Local DNS Server with CoreDNS

scutil handles the routing, but you still need a DNS server running on port 1053. I used CoreDNS for this.

brew install coredns

Create a Corefile to define what should resolve and how:

myapp.test {
    hosts {
        127.0.0.1 myapp.test
        127.0.0.1 api.myapp.test
        127.0.0.1 auth.myapp.test
        fallthrough
    }
    log
    errors
    bind 127.0.0.1
}

. {
    forward . 8.8.8.8
    bind 127.0.0.1
}

Start CoreDNS on port 1053 (non-root since 1053 is above 1023):

coredns -conf Corefile -dns.port 1053

Test that the resolver works:

dig myapp.test @localhost -p 1053

You should see status: NOERROR and an ANSWER SECTION pointing to 127.0.0.1. Without specifying the server, dig myapp.test will still return NXDOMAIN because your public DNS has no record for it. That is expected.

Flushing DNS Cache

When you add or remove resolvers, flush the DNS cache so changes take effect immediately:

sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

Why Not dnsmasq?

dnsmasq is another popular choice. It works, but I found CoreDNS easier to configure with a single Corefile and it has better observability out of the box (structured logging, health endpoint). Either works with the scutil approach described here.

Summary

The key insight is that /etc/resolver/ files are not the primary mechanism for scoped DNS on macOS. They are a compatibility shim. The actual DNS configuration lives in the System Configuration framework, and scutil is the tool to write to it. Combine that with CoreDNS running on a local port and you get a flexible local DNS setup that does not require editing /etc/hosts for every new subdomain.