
How does DNS work inside Docker and Kubernetes?
Inside Docker, containers on a user-defined network use Docker's embedded DNS server at 127.0.0.11, which resolves other containers and Compose services by name and forwards everything else to the host's upstream resolvers. Inside Kubernetes, every pod's /etc/resolv.conf points at the cluster DNS service, usually CoreDNS, which answers for names such as my-service.my-namespace.svc.cluster.local from the Kubernetes API and forwards external names upstream. In both cases, search domains in resolv.conf let you use short names like db or my-service, and that convenience is also behind most container DNS problems.
Container DNS looks like magic until something stops resolving. This article explains how Docker resolves names on the default bridge and on user-defined networks, how Docker Compose service discovery works, how Kubernetes builds DNS records for services and pods, what ndots:5 does and why it matters, how to customise DNS for a container or pod, and a practical toolkit for debugging resolution failures.
DNS in Docker
Docker's DNS behaviour depends on which network a container is attached to.
The Default Bridge Network
Containers started without a --network option join the default bridge network. There, Docker doesn't provide name-based discovery between containers. Instead, it copies the host's /etc/resolv.conf into the container, with one adjustment: if the host uses a local stub resolver such as 127.0.0.53 (common with systemd-resolved), that address wouldn't work inside the container, so Docker uses the real upstream servers instead, or falls back to public resolvers if it can't find any.
docker run --rm alpine cat /etc/resolv.conf
nameserver 192.0.2.53
search example.internal
Containers on the default bridge can reach each other by IP address, but not by name. The old --link option is a legacy feature and shouldn't be used for new work.
User-Defined Networks and the Embedded DNS Server
On a user-defined network, Docker runs an embedded DNS server that every container reaches at 127.0.0.11:
docker network create appnet
docker run -d --name db --network appnet -e POSTGRES_PASSWORD=example postgres:17
docker run --rm --network appnet alpine cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
The embedded server:
- Resolves container names, such as
db, to the container's IP address on that network. - Resolves network aliases you assign with
--network-alias. - Forwards all other queries to the upstream resolvers Docker picked from the host, or the ones you set with
--dns.
You can see it working with a quick lookup:
docker run --rm --network appnet alpine nslookup db
Server: 127.0.0.11
Address: 127.0.0.11:53
Non-authoritative answer:
Name: db
Address: 172.18.0.2
Names only resolve between containers that share a network. A container on appnet can't resolve containers on another network unless it's attached to both.
Round-Robin Aliases
If several containers share the same network alias, the embedded DNS server returns all of their addresses, which gives you simple DNS round robin:
docker run -d --network appnet --network-alias web nginx:1.27
docker run -d --network appnet --network-alias web nginx:1.27
docker run --rm --network appnet alpine nslookup web
Client libraries differ in how they use multiple addresses, so don't treat this as a real load balancer.
Docker Compose Service Discovery
Docker Compose creates a user-defined network for each project by default, so every service can reach every other service by its service name:
# compose.yaml
services:
web:
image: nginx:1.27
ports:
- "8080:80"
depends_on:
- api
api:
image: ghcr.io/example/api:1.4.0
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
Here, api connects to the database using the hostname db. If you scale a service with docker compose up --scale api=3, the name api resolves to all three container addresses.
Customising DNS for Containers
You can override DNS settings per container:
docker run --rm \
--dns 1.1.1.1 \
--dns-search example.internal \
--dns-option ndots:2 \
--add-host legacy.example.internal:192.0.2.40 \
alpine cat /etc/resolv.conf /etc/hosts
The Compose equivalents are dns, dns_search, dns_opt and extra_hosts:
services:
worker:
image: ghcr.io/example/worker:2.0.1
dns:
- 1.1.1.1
- 9.9.9.9
dns_search:
- example.internal
extra_hosts:
- "legacy.example.internal:192.0.2.40"
On a user-defined network, --dns doesn't replace 127.0.0.11 in the container. It changes the upstream servers the embedded DNS server forwards to, so container names keep working.
To change the defaults for every container on a host, set dns in /etc/docker/daemon.json and restart Docker:
{
"dns": ["192.0.2.53", "1.1.1.1"]
}
DNS in Kubernetes
Kubernetes has DNS built into its design. Every cluster runs a DNS add-on, almost always CoreDNS, deployed in the kube-system namespace behind a Service traditionally named kube-dns.
kubectl -n kube-system get svc kube-dns
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 41d
The kubelet writes that ClusterIP into every pod's /etc/resolv.conf.
What a Pod's resolv.conf Looks Like
For a pod in the shop namespace with the default DNS policy:
kubectl -n shop exec deploy/frontend -- cat /etc/resolv.conf
search shop.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
- nameserver: The cluster DNS service.
- search: Domains appended to short names, starting with the pod's own namespace.
- ndots:5: Names with fewer than five dots are tried with each search domain first.
Records Kubernetes Creates
CoreDNS answers from the Kubernetes API using its kubernetes plugin. The main record types are:
| Object | Name | Answer |
|---|---|---|
| ClusterIP Service | my-svc.my-ns.svc.cluster.local | A/AAAA with the Service's ClusterIP |
Headless Service (clusterIP: None) | my-svc.my-ns.svc.cluster.local | A/AAAA for each ready pod behind it |
| StatefulSet pod via headless Service | web-0.my-svc.my-ns.svc.cluster.local | A/AAAA for that specific pod |
| Named Service port | _http._tcp.my-svc.my-ns.svc.cluster.local | SRV with port and target |
| ExternalName Service | my-svc.my-ns.svc.cluster.local | CNAME to the external hostname |
cluster.local is the default cluster domain. Some clusters use a different one, which you'll see in the search line.
Because of the search domains, a pod in shop can reach a Service in the same namespace as just payments, and one in another namespace as payments.billing or payments.billing.svc.
Headless Services and StatefulSets
Headless Services are how databases and other stateful workloads get stable per-pod names:
apiVersion: v1
kind: Service
metadata:
name: pg
namespace: data
spec:
clusterIP: None
selector:
app: pg
ports:
- name: postgres
port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg
namespace: data
spec:
serviceName: pg
replicas: 2
selector:
matchLabels:
app: pg
template:
metadata:
labels:
app: pg
spec:
containers:
- name: postgres
image: postgres:17
env:
- name: POSTGRES_PASSWORD
value: example-only
ports:
- containerPort: 5432
name: postgres
Each replica then has its own name, such as pg-0.pg.data.svc.cluster.local and pg-1.pg.data.svc.cluster.local, while pg.data.svc.cluster.local returns the addresses of all ready replicas.
The CoreDNS Corefile
CoreDNS is configured through a ConfigMap called coredns in kube-system. A typical default looks like this:
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
The key lines are kubernetes, which serves cluster names, forward, which sends everything else to the node's upstream resolvers, and cache, which caches answers for up to 30 seconds.
To send an internal corporate domain to your own DNS servers, add a separate server block:
corp.example.com:53 {
errors
cache 30
forward . 192.0.2.53 192.0.2.54
}
Managed Kubernetes services sometimes manage this ConfigMap for you and provide a separate place for custom configuration, so check your platform's documentation before editing it directly.
Why ndots:5 Matters
With ndots:5, any name with fewer than five dots is treated as possibly relative. When a pod in shop looks up api.example.com (two dots), the resolver library tries the search domains first:
api.example.com.shop.svc.cluster.localapi.example.com.svc.cluster.localapi.example.com.cluster.localapi.example.com
Each attempt is usually a pair of A and AAAA queries, so one external lookup can become eight queries, with the first six returning NXDOMAIN. At scale, that adds latency and load on CoreDNS.
Ways to reduce it:
- Use fully qualified names with a trailing dot for external hosts in configuration, such as
api.example.com., which skips the search list. Test this with your application, as some libraries handle trailing dots poorly. - Lower ndots for pods that mainly talk to external services:
apiVersion: v1
kind: Pod
metadata:
name: external-client
spec:
dnsConfig:
options:
- name: ndots
value: "2"
containers:
- name: app
image: ghcr.io/example/client:3.2.0
- Run NodeLocal DNSCache, an add-on that runs a caching DNS agent on every node, commonly listening on
169.254.20.10, so most queries never leave the node.
Pod DNS Policies
The dnsPolicy field controls how a pod's DNS is set up:
| Policy | Behaviour |
|---|---|
ClusterFirst (default) | Use cluster DNS. Non-cluster names are forwarded upstream by CoreDNS. |
Default | Inherit the node's DNS configuration, without cluster names. |
ClusterFirstWithHostNet | Use cluster DNS for pods running with hostNetwork: true. |
None | Ignore cluster settings and use only what you set in dnsConfig. |
A pod with fully custom DNS:
apiVersion: v1
kind: Pod
metadata:
name: custom-dns
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- 192.0.2.53
searches:
- corp.example.com
options:
- name: ndots
value: "1"
containers:
- name: app
image: alpine:3.20
command: ["sleep", "3600"]
Pods with hostNetwork: true default to the node's DNS. If they need to resolve Services, set dnsPolicy: ClusterFirstWithHostNet.
Debugging DNS in Containers
Most DNS problems in containers fall into a few categories: the name is wrong, the network is wrong, the resolver isn't reachable, or upstream resolution is failing.
Docker
# Is the container on the network you think it is?
docker inspect -f '{{json .NetworkSettings.Networks}}' api | jq 'keys'
# What does the container's resolver config say?
docker exec api cat /etc/resolv.conf
# Resolve from a throwaway container on the same network
docker run --rm --network appnet nicolaka/netshoot dig db
If container names resolve but external names don't, the problem is the upstream resolver Docker picked. Check the host's DNS configuration and the dns setting in daemon.json. Corporate VPNs and firewalls blocking outbound port 53 are frequent culprits.
Kubernetes
Start a debugging pod with DNS tools:
kubectl run dnsutils --rm -it --restart=Never \
--image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3 -- bash
Inside it:
nslookup kubernetes.default
dig payments.billing.svc.cluster.local +search
dig api.example.com
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: kubernetes.default.svc.cluster.local
Address: 10.96.0.1
If kubernetes.default fails, check CoreDNS itself:
kubectl -n kube-system get pods -l k8s-app=kube-dns
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50
kubectl -n kube-system get endpointslices -l kubernetes.io/service-name=kube-dns
Common causes include:
- CoreDNS pods crashing, often because of a forwarding loop detected by the
loopplugin when the node'sresolv.confpoints back at a local stub resolver. - Network policies blocking egress to
kube-systemon port 53. If you use default-deny policies, explicitly allow DNS traffic over both UDP and TCP. - A Service with no ready endpoints. Headless Services return no addresses when no pods are ready.
- The wrong namespace. A short name only resolves within the pod's own namespace.
A network policy that allows DNS to the cluster DNS pods looks like this:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: shop
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Docker and Kubernetes DNS Compared
| Aspect | Docker (user-defined network) | Kubernetes |
|---|---|---|
| Resolver address | 127.0.0.11 inside each container | Cluster DNS Service IP, such as 10.96.0.10 |
| Server software | Built into the Docker engine | CoreDNS deployment |
| Names | Container names, service names, aliases | Services, headless Service pods, SRV records |
| Default ndots | 0 | 5 |
| Scope | Containers on the same network | Whole cluster, namespaced names |
| Customisation | --dns, daemon.json, Compose options | dnsPolicy, dnsConfig, CoreDNS Corefile |
FAQ: DNS in Docker and Kubernetes
It's the address of Docker's embedded DNS server, which is available to containers on user-defined networks. It resolves container and service names on the same network and forwards other queries to upstream resolvers.
They're probably on the default bridge network, which doesn't provide name resolution between containers. Create a user-defined network and attach both containers to it, or use Docker Compose, which does this automatically.
It follows the pattern service-name.namespace.svc.cluster.local, where cluster.local is the default cluster domain. Within the same namespace, the short service name is enough.
For pods that mostly call external hostnames, lowering ndots to 2 or using fully qualified names can significantly reduce DNS queries. Test carefully, because short Service names that contain dots may then skip the search list.
Docker chooses upstream resolvers when the container starts, often from the host's configuration. A VPN may change the host's resolvers or block outbound DNS. Restart containers after connecting, or set explicit dns servers in daemon.json.
kube-dns was the original Kubernetes DNS add-on. CoreDNS replaced it as the default, but the Service is still usually called kube-dns for compatibility, so existing configurations keep working.
Conclusion
Docker and Kubernetes both give containers a local view of DNS. Docker's embedded server at 127.0.0.11 handles container and Compose service names on user-defined networks, while Kubernetes uses CoreDNS to publish Services, headless Service pods and SRV records under the cluster domain, with search domains making short names work.
When things go wrong, check which network or namespace the workload is in, read its /etc/resolv.conf, and test from a debugging container with dig or nslookup. Understanding search domains and ndots, and allowing DNS through network policies, will prevent most container DNS issues before they start.


