How to Set a Persistent DNS Server on macOS When Offline for Local Development


14 views

When working on local development projects (especially Rails apps using subdomains), macOS's handling of DNS can be frustrating. The system clears /etc/resolv.conf when offline and ignores manual edits. This breaks local DNS servers like Dnsmasq that rely on these settings.

Standard approaches don't work:

  • Editing /etc/resolv.conf - gets overwritten
  • Network Preferences GUI - disabled when offline
  • scutil commands - often fail without network

Create a custom configuration for macOS's DNS resolver:

sudo mkdir -p /etc/resolver
sudo tee /etc/resolver/local <<EOF
nameserver 127.0.0.1
domain local
search_order 1
EOF

Here's a complete local development setup:

# Install Dnsmasq
brew install dnsmasq

# Configuration
echo 'address=/.test/127.0.0.1' >> /usr/local/etc/dnsmasq.conf
echo 'listen-address=127.0.0.1' >> /usr/local/etc/dnsmasq.conf

# Start the service
sudo brew services start dnsmasq

Verify it works offline:

# Turn off all network interfaces
networksetup -setairportpower airport off
networksetup -setnetworkserviceenabled "Thunderbolt Ethernet" off

# Test resolution
dig anyname.test @127.0.0.1
ping -c 1 anyname.test

For projects needing many subdomains:

# In dnsmasq.conf
address=/.localhost/127.0.0.1
address=/.lvh.me/127.0.0.1
address=/.xip.io/127.0.0.1

This persists through network changes and works offline.


When working on local development (especially with Rails subdomains), macOS aggressively clears /etc/resolv.conf during network interface changes. This breaks local DNS solutions like Dnsmasq when going offline.

For Rails apps using wildcard subdomains (*.yourapp.local), /etc/hosts can't help because:

# Doesn't work for wildcards
127.0.0.1 account1.yourapp.local
127.0.0.1 account2.yourapp.local
# ...but impractical for dynamic subdomains

First, ensure proper Dnsmasq configuration:

# /usr/local/etc/dnsmasq.conf
address=/.yourapp.local/127.0.0.1
listen-address=127.0.0.1

Create a launchd plist to enforce DNS settings regardless of network state:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>local.dns.enforcer</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/sbin/networksetup</string>
        <string>-setdnsservers</string>
        <string>Wi-Fi</string>
        <string>127.0.0.1</string>
    </array>
    <key>StartInterval</key>
    <integer>30</integer>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

For newer macOS versions, modify the mDNSResponder config:

sudo mkdir -p /etc/resolver
sudo tee /etc/resolver/local <<EOF
nameserver 127.0.0.1
domain yourapp.local
search_order 1
EOF

Check active DNS configuration:

scutil --dns | grep 'nameserver'
dig test.yourapp.local @127.0.0.1

Create a test controller to verify subdomain handling:

# Rails controller example
class SubdomainTestController < ApplicationController
  def check
    render plain: "Serving from: #{request.subdomain}"
  end
end

Then test with:

curl http://random123.yourapp.local:3000/subdomain_test/check
# Should return "Serving from: random123"