Local DNS for *.k8s.local
The /etc/hosts approach works for a handful of domains, but every new
IngressRoute means another line to add manually. A local DNS server handles
*.k8s.local with a single wildcard rule and survives new services without
edits.
This guide uses dnsmasq behind systemd-resolved. Resolved continues handling
all normal DNS — dnsmasq only sees queries for k8s.local.
Install dnsmasq
Section titled “Install dnsmasq”# Fedorasudo dnf install dnsmasq
# Debian/Ubuntusudo apt install dnsmasqConfigure dnsmasq
Section titled “Configure dnsmasq”systemd-resolved already owns port 53 on 127.0.0.53, so dnsmasq listens on a different loopback address.
sudo tee /etc/dnsmasq.d/k8s-local.conf << 'EOF'# Listen on a separate loopback addresslisten-address=127.0.0.2bind-interfaces
# Don't forward unknown queries upstream (only answer what we know)no-resolv
# Individual recordsaddress=/docs.k8s.local/127.0.0.1address=/grafana.k8s.local/127.0.0.1address=/traefik.k8s.local/127.0.0.1address=/pgweb.k8s.local/127.0.0.1
# Or wildcard: send ALL *.k8s.local to one IP# address=/k8s.local/127.0.0.1EOFThe wildcard form (address=/k8s.local/127.0.0.1) matches every subdomain. Use
individual records if different services resolve to different IPs, or the
wildcard if everything goes through the same Traefik port-forward on localhost.
Start dnsmasq
Section titled “Start dnsmasq”sudo systemctl restart dnsmasqsudo systemctl enable dnsmasqRoute .k8s.local queries to dnsmasq
Section titled “Route .k8s.local queries to dnsmasq”Tell systemd-resolved to send .k8s.local queries to the dnsmasq instance:
sudo resolvectl dns lo 127.0.0.2sudo resolvectl domain lo "~k8s.local"The ~ prefix means “routing domain” — resolved sends any query matching
*.k8s.local to the DNS server on that interface (127.0.0.2), but doesn’t
add k8s.local as a search domain.
Using k8s.local as a search domain
Section titled “Using k8s.local as a search domain”If you want short names like docs to resolve as docs.k8s.local, drop the tilde:
sudo resolvectl domain lo "k8s.local"Without ~, it acts as both a routing domain and a search domain. Now curl http://docs resolves as docs.k8s.local.
Make it persistent
Section titled “Make it persistent”The resolvectl commands don’t survive a reboot. Create a systemd-networkd config:
sudo tee /etc/systemd/network/10-k8s-local-dns.network << 'EOF'[Match]Name=lo
[Network]DNS=127.0.0.2Domains=~k8s.localEOFRestart networking:
sudo systemctl restart systemd-networkdVerify
Section titled “Verify”# Check resolved sees the routing domainresolvectl status lo
# Test a lookupresolvectl query docs.k8s.localThe query should resolve via 127.0.0.2 with the IP configured in dnsmasq.
Adding new records
Section titled “Adding new records”Edit /etc/dnsmasq.d/k8s-local.conf and restart dnsmasq:
sudo systemctl restart dnsmasqNo need to touch /etc/hosts, resolved config, or the networkd file again. If
you used the wildcard rule, new subdomains resolve immediately with no changes
at all.
Alternative: cluster-side DNS with CoreDNS
Section titled “Alternative: cluster-side DNS with CoreDNS”Instead of running dnsmasq on the host, deploy a DNS server inside the cluster
and point your machine at it. This gives you automatic resolution of Kubernetes
service names — hello-app.demo.svc.cluster.local resolves without any
host-side configuration changes when you add new services.
The CoreDNS Helm chart is maintained by the CoreDNS project (CNCF graduated). You deploy a second CoreDNS instance alongside the built-in one, configured as a NodePort service so your host can reach it.
Install the CoreDNS Helm chart
Section titled “Install the CoreDNS Helm chart”helm repo add coredns https://coredns.github.io/helmhelm install coredns-external coredns/coredns \ --namespace kube-system \ --set isClusterService=false \ --set serviceType=NodePort \ --set service.clusterIP="" \ --set "service.nodePort=31053" \ --set "servers[0].zones[0].zone=." \ --set "servers[0].port=53" \ --set "servers[0].plugins[0].name=kubernetes" \ --set "servers[0].plugins[0].parameters=cluster.local in-addr.arpa ip6.arpa" \ --set "servers[0].plugins[1].name=forward" \ --set "servers[0].plugins[1].parameters=. /etc/resolv.conf"This creates a CoreDNS instance that resolves *.cluster.local via the
Kubernetes API and forwards everything else upstream. It listens on NodePort
31053.
Expose the port through Kind
Section titled “Expose the port through Kind”Add extraPortMappings to your Kind cluster config so NodePort 31053 is
accessible on localhost:
kind: ClusterapiVersion: kind.x-k8s.io/v1alpha4nodes: - role: control-plane extraPortMappings: - containerPort: 31053 hostPort: 31053 protocol: UDP - containerPort: 31053 hostPort: 31053 protocol: TCPPoint your host at the cluster DNS
Section titled “Point your host at the cluster DNS”macOS uses resolver files in /etc/resolver/. Create one for cluster.local:
sudo mkdir -p /etc/resolversudo tee /etc/resolver/cluster.local << 'EOF'nameserver 127.0.0.1port 31053EOFmacOS checks /etc/resolver/<domain> before the system DNS for matching queries. Any lookup ending in .cluster.local goes to the CoreDNS instance in your Kind cluster.
This file persists across reboots. Remove it when you no longer need cluster DNS resolution:
sudo rm /etc/resolver/cluster.localRoute cluster.local queries to the CoreDNS NodePort. The approach is the same as the dnsmasq setup earlier — tell resolved about a new routing domain:
sudo resolvectl dns lo 127.0.0.1:31053sudo resolvectl domain lo "~cluster.local"To persist across reboots, add a networkd config:
sudo tee /etc/systemd/network/10-k8s-cluster-dns.network << 'EOF'[Match]Name=lo
[Network]DNS=127.0.0.1:31053Domains=~cluster.localEOFsudo systemctl restart systemd-networkdVerify
Section titled “Verify”Test a lookup against a known service. Every cluster has kubernetes.default.svc.cluster.local:
dig @127.0.0.1 -p 31053 kubernetes.default.svc.cluster.localIf you have services deployed, resolve them by their full DNS name:
# Format: <service>.<namespace>.svc.cluster.localcurl http://hello-app.demo.svc.cluster.localNew services resolve automatically — no config files to edit, no dnsmasq to restart. The cluster’s CoreDNS picks them up the moment the Service object exists.