How to Configure DNS for Developing Webapp with Dnsmaqs once for all
When developing web applications locally, managing domain names quickly becomes a tedious chore. Whether you are running multiple projects simultaneously or using tools that require specific domain names (such as multi-tenant applications), you need a way to map local domains to your machine.
For years, the standard approach was to use dnsmasq to automatically route a specific TLD (like .test or .localhost) to 127.0.0.1. However, modern Ubuntu releases changed how DNS is handled out of the box, leading to a major port conflict.
Here is a complete guide on why we use dnsmasq, what goes wrong on modern systems, and how to configure a clean, modern solution.
Section intitulée the-goal-dynamic-local-domainsThe Goal: Dynamic Local Domains
In a basic setup, developers often rely on the /etc/hosts file to map a domain to localhost:
127.0.0.1 my-awesome-app.test
127.0.0.1 api.my-awesome-app.test
While this works, it does not scale. Every time you create a new project, add a subdomain, or change a local domain, you have to edit this system file manually with root privileges.
This is where dnsmasq comes in.
dnsmasq is a lightweight DNS forwarder. Instead of hardcoding every single domain name, it allows you to define a wildcard rule. For example, you can tell your system: “Any query that ends with .test should automatically resolve to 127.0.0.1.”
Once configured, you can access anything.test, project-a.test, or sub.project-b.test instantly without ever touching a configuration file again.
Section intitulée the-problem-enter-systemd-resolvedThe Problem: Enter systemd-resolved
Historically, setting up dnsmasq involved making it the primary DNS resolver for your entire system.
However, modern Ubuntu releases now rely on systemd-resolved to handle network-level DNS queries. systemd-resolved runs a local DNS stub listener that binds to 127.0.0.53 on Port 53.
When you install dnsmasq and try to start it, the service will crash with the following error:
failed to create listening socket for port 53: Address already in use
Because dnsmasq attempts to bind to Port 53 on all available network interfaces by default, it collides directly with systemd-resolved.
In the past, many workarounds suggested disabling systemd-resolved entirely. However, doing so is highly discouraged as it often breaks DNS resolution when switching Wi-Fi networks, connecting to VPNs, or dealing with captive portals.
Section intitulée the-solution-elegant-cohabitationThe Solution: Elegant Cohabitation
The most robust and modern solution is to let both services cohabitate. We will restrict dnsmasq to only listen on the standard loopback address (127.0.0.1) and instruct systemd-resolved to forward queries for our development TLD to it.
Section intitulée 1-install-dnsmasq1. Install dnsmasq
If you haven’t already, install dnsmasq via your package manager. Note that the service might fail to start immediately after installation due to the port conflict—this is normal, and we will fix it in the next steps.
sudo apt update && sudo apt install dnsmasq
Section intitulée 2-configure-and-restrict-dnsmasq2. Configure and restrict dnsmasq
Create or edit your local configuration file (for example, /etc/dnsmasq.d/dev-domains.conf).
First, we force dnsmasq to only bind to the local loopback interface (127.0.0.1), leaving the rest of the network stack alone. Second, we define our wildcard routing rule for .test:
sudo tee -a /etc/dnsmasq.d/dev-domains.conf <<EOF
# Restrict dnsmasq to the local loopback address
listen-address=127.0.0.1
interface=lo
bind-interfaces
# Configure the wildcard TLD
address=/.test/127.0.0.1
EOF
Section intitulée 3-configure-routing-in-systemd-resolved3. Configure routing in systemd-resolved
Now we need to tell systemd-resolved that it should delegate any DNS queries ending in .test to our local dnsmasq instance running on 127.0.0.1.
Create a new configuration directory and file:
sudo mkdir -p /etc/systemd/resolved.conf.d/
sudo tee -a /etc/systemd/resolved.conf.d/dnsmasq.conf <<EOF
[Resolve]
DNS=127.0.0.1
Domains=~test
EOF
Note: The
~symbol beforetestis critical. It defines a “routing domain”. This tellssystemd-resolvedto use the specified DNS IP (127.0.0.1) only for queries matching that specific domain. All other internet traffic will continue to use your standard network DNS servers.
Section intitulée 4-restart-the-services4. Restart the services
Apply the changes by restarting both system daemons:
sudo systemctl restart dnsmasq systemd-resolved
Section intitulée conclusionConclusion
By configuring a routing domain, you get the best of both worlds. You no longer need to manually edit or override the system’s /etc/resolv.conf symlink.
Your local development domains resolve instantly, and your system’s network configuration remains stable and untouched across updates, VPN connections, and network changes.