Peter Hoffmann

Local macOS Dev Setup: dnsmasq + caddy-snake for python projects

When working on a single web project, running flask run on a fixed port is usually more than sufficient. However, as soon as you start developing multiple services in parallel, this approach quickly becomes cumbersome: ports collide, you have to remember which service runs on which port, and you end up constantly starting, stopping, and restarting individual development servers by hand.

Using a wildcard local domain (*.lan) throuhg dnsmask and a vhost proxy with proper WSGI services solves these problems cleanly. Each project gets a stable, memorable local subdomain instead of a port number, services can run side by side without collisions, and process management becomes centralized and predictable. The result is a local development setup with less friction.

dnsmasq on macOS Sonoma (Local DNS with .lan)

This is a concise summary of how to install and configure dnsmasq on macOS Sonoma to resolve local development domains using a .lan wildcard (e.g. *.lan → 127.0.0.1).

1. Install dnsmasq

Using Homebrew:

brew install dnsmasq

Homebrew (on Apple Silicon) installs dnsmasq and places the default config in:

/opt/homebrew/etc/dnsmasq.conf

2. Configure dnsmasq

Edit the configuration file:

sudo vim /opt/homebrew/etc/dnsmasq.conf

Add the following:

# Listen only on localhost
listen-address=127.0.0.1
bind-interfaces

# DNS port
port=53

# Wildcard domain for local development
address=/.lan/127.0.0.1

This maps any *.lan hostname to 127.0.0.1.

It's recommended to not use .dev as this is real Google owned TLD and browsers have baked in to use HTTPS only. Also don't use .local as this is reserved for mDNS (Bonjour).

3. Tell macOS to use dnsmasq

macOS ignores /etc/resolv.conf, so DNS must be configured per network interface.

Option A: System Settings (GUI)

  1. System Settings → Network
  2. Select your active interface (Wi-Fi / Ethernet)
  3. Details → DNS
  4. Add:
    127.0.0.1
    
  5. Move it to the top of the DNS server list

Option B: Command line

networksetup -setdnsservers Wi-Fi 127.0.0.1

(Replace Wi-Fi with the correct interface name if needed.)

4. Start dnsmasq

Run it as a background service:

sudo brew services start dnsmasq

Or run it manually for debugging:

sudo dnsmasq --no-daemon

5. Flush DNS cache

This step is required on Sonoma:

sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

6. Test

dig foo.lan
ping foo.lan

Both should resolve to:

127.0.0.1

Result

You now have:

Caddy

Note: My initial plan was to use caddy with caddy-snake to run multiple vhosts for python wsgi apps with the configuration below. But this did not work out as expected because caddy-snake does not run multiple python interpreters for the different projects, but only appends the site packages from the python projects to sys.path for all projects and runs all of them in the same python interpreter. This leads to problems with different python versions or incompatible python requirements installed in the different venv versions. So the approach below only works if you use the same python version and your requirements are compatible within the different apps.

Caddyfile: host multiple WSGI services

As we want caddy to run wsgi services we need to build caddy-snake:

The caddyfile now needs to be stored in $(brew --prefix)/etc/Caddyfile

{
    auto_https off
}

http://foo.lan {
    bind 127.0.0.1
    route {
        python {
            module_wsgi app:app
            working_dir /Users/you/dev/foo
            venv /Users/you/dev/foo/.venv
		}
	}

    log {
        output stdout
        format console
    }
}

http://bar.lan {
    bind 127.0.0.1 
    route {
        python {
            module_wsgi app:app
            working_dir /Users/you/dev/bar
            venv /Users/you/dev/bar/.venv
        }
    }
}