Type something to search...
How does DNS work inside Docker and Kubernetes?

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:

ObjectNameAnswer
ClusterIP Servicemy-svc.my-ns.svc.cluster.localA/AAAA with the Service's ClusterIP
Headless Service (clusterIP: None)my-svc.my-ns.svc.cluster.localA/AAAA for each ready pod behind it
StatefulSet pod via headless Serviceweb-0.my-svc.my-ns.svc.cluster.localA/AAAA for that specific pod
Named Service port_http._tcp.my-svc.my-ns.svc.cluster.localSRV with port and target
ExternalName Servicemy-svc.my-ns.svc.cluster.localCNAME 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:

  1. api.example.com.shop.svc.cluster.local
  2. api.example.com.svc.cluster.local
  3. api.example.com.cluster.local
  4. api.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:

PolicyBehaviour
ClusterFirst (default)Use cluster DNS. Non-cluster names are forwarded upstream by CoreDNS.
DefaultInherit the node's DNS configuration, without cluster names.
ClusterFirstWithHostNetUse cluster DNS for pods running with hostNetwork: true.
NoneIgnore 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 loop plugin when the node's resolv.conf points back at a local stub resolver.
  • Network policies blocking egress to kube-system on 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

AspectDocker (user-defined network)Kubernetes
Resolver address127.0.0.11 inside each containerCluster DNS Service IP, such as 10.96.0.10
Server softwareBuilt into the Docker engineCoreDNS deployment
NamesContainer names, service names, aliasesServices, headless Service pods, SRV records
Default ndots05
ScopeContainers on the same networkWhole cluster, namespaced names
Customisation--dns, daemon.json, Compose optionsdnsPolicy, 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.

Tags :
Share :

Related Posts

What is the difference between an A record and a CNAME record?

What is the difference between an A record and a CNAME record?

The difference between an A record and a CNAME record is what they point to. An A record points a hostname directly at an IPv4 address, such

Dive Deeper
What are the best DNS providers for websites?

What are the best DNS providers for websites?

The best DNS providers for most websites are Cloudflare for a fast, free, easy starting point, Amazon Route 53 if you already run on AWS, **G

Dive Deeper
What are the best public DNS resolvers?

What are the best public DNS resolvers?

The best public DNS resolvers for most people are Cloudflare (1.1.1.1) for speed and privacy, Google Public DNS (8.8.8.8) for reliability and

Dive Deeper