CoreDNS
CoreDNS is the cluster DNS server in every Kubernetes cluster since 1.13. It replaced kube-dns (dnsmasq + kubedns + sidecar). It runs as a Deployment in kube-system, fronted by a Service with a stable ClusterIP that all pods use for DNS.
How Pod DNS Resolution Works
Every pod gets /etc/resolv.conf auto-injected by kubelet:
nameserver 10.96.0.10 ← CoreDNS ClusterIP
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
When a pod does curl postgres:
sequenceDiagram
participant App as App (pod)
participant R as libc resolver
participant CD as CoreDNS
App->>R: resolve "postgres"
Note over R: ndots:5 — "postgres" has 0 dots < 5<br/>try search domains first
R->>CD: postgres.default.svc.cluster.local?
CD-->>R: 10.96.12.34 ✓ (if service exists)
R-->>App: 10.96.12.34
Note over R: If service didn't exist, it would continue:
R->>CD: postgres.svc.cluster.local?
CD-->>R: NXDOMAIN
R->>CD: postgres.cluster.local?
CD-->>R: NXDOMAIN
R->>CD: postgres. (absolute)
CD-->>R: NXDOMAIN
Best case: 1 DNS query (service found on first search domain). Worst case: 4–5 queries for a name that doesn't exist as a K8s service.
The ndots:5 Problem
ndots:5 means: if the hostname has fewer than 5 dots, the resolver tries all search domains before attempting the name as-is.
Concrete example — curl https://api.external.com/v1/data:
api.external.comhas 2 dots < 5- Resolver tries:
api.external.com.default.svc.cluster.local→ NXDOMAIN - Then:
api.external.com.svc.cluster.local→ NXDOMAIN - Then:
api.external.com.cluster.local→ NXDOMAIN - Finally:
api.external.com.→ answer
That's 4 DNS queries for every outbound HTTPS call. At 1000 RPS this is 4000 DNS queries/sec extra load on CoreDNS.
Fixes
Fix 1 — Use FQDN (trailing dot):
# In app config: always use FQDN for K8s services
postgres.default.svc.cluster.local. # trailing dot = absolute, no search
Fix 2 — Reduce ndots in pod spec:
spec:
dnsConfig:
options:
- name: ndots
value: "2" # only search if fewer than 2 dots
# Now "postgres" still searches (0 dots < 2)
# But "api.external.com" (2 dots) goes direct — no search domain iteration
Fix 3 — Use fully qualified service names in app config:
DB_HOST=postgres.default.svc.cluster.local
Corefile — CoreDNS Configuration
CoreDNS config lives in a ConfigMap:
kubectl get configmap coredns -n kube-system -o yaml
Default Corefile:
.:53 {
errors # log errors to stdout
health { # /health endpoint on :8080
lameduck 5s # wait 5s after marking unhealthy before shutting down
}
ready # /ready endpoint on :8181 — returns 200 when all plugins ready
kubernetes cluster.local in-addr.arpa ip6.arpa { # handle cluster.local DNS
pods insecure # enable pod DNS records (insecure = no verification)
fallthrough in-addr.arpa ip6.arpa # pass reverse lookups to next plugin
ttl 30 # TTL for cluster.local records
}
prometheus :9153 # expose metrics at /metrics
forward . /etc/resolv.conf { # forward non-cluster queries to node's resolver
max_concurrent 1000
}
cache 30 # cache all responses for 30s
loop # detect forwarding loops, panic if found
reload # hot-reload Corefile on ConfigMap change
loadbalance # round-robin DNS (randomize A record order)
}
Plugin reference
| Plugin | Purpose |
|---|---|
errors |
Log error responses |
health |
Liveness endpoint /health on :8080 |
ready |
Readiness endpoint /ready on :8181 |
kubernetes |
Serve DNS for cluster services/pods |
prometheus |
Metrics endpoint :9153 |
forward |
Forward upstream queries |
cache |
Response caching with TTL |
loop |
Detect + break forwarding loops |
reload |
Watch ConfigMap, hot-reload without restart |
loadbalance |
Randomize record order for basic LB |
Custom Forwarding
Forward a specific domain to an internal DNS server:
.:53 {
errors
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
# Forward corp.example.com to internal DNS
forward corp.example.com 10.0.1.53 {
prefer_udp
}
# Forward everything else to public resolvers
forward . 8.8.8.8 8.8.4.4 {
max_concurrent 1000
health_check 5s
}
cache 30
loop
reload
loadbalance
}
# Apply the new ConfigMap — reload plugin picks it up automatically
kubectl apply -f coredns-configmap.yaml
# Verify reload happened:
kubectl logs -n kube-system -l k8s-app=kube-dns | grep "Reloading"
Caching
The cache plugin caches all DNS responses in memory.
cache 30 # 30s TTL for positive responses, 15s for NXDOMAIN (half of positive)
# or
cache {
success 9984 300 60 # max entries, max TTL, min TTL for positive
denial 9984 300 5 # max entries, max TTL, min TTL for NXDOMAIN
prefetch 10 1m 10% # prefetch entries accessed >10 times before 10% of TTL remains
}
Effect: service.namespace.svc.cluster.local first lookup hits K8s API → cached for 30s → subsequent lookups served from memory without hitting API server.
dnsPolicy Options
| Policy | Behaviour |
|---|---|
ClusterFirst (default) |
Use CoreDNS. Non-cluster names forwarded upstream. |
ClusterFirstWithHostNet |
Same as ClusterFirst but for pods using hostNetwork: true |
Default |
Use the node's /etc/resolv.conf — no cluster DNS |
None |
No DNS config injected — you must set dnsConfig manually |
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- 10.96.0.10
searches:
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "2"
- name: timeout
value: "2"
- name: attempts
value: "3"
Scaling CoreDNS
Default: 2 replicas. Under heavy load you'll see CoreDNS CPU spike and DNS latency rise.
# Scale manually
kubectl scale deployment coredns -n kube-system --replicas=4
# Or use CoreDNS Autoscaler (proportional to nodes/cores)
kubectl get configmap coredns-autoscaler -n kube-system -o yaml
# Data: {"linear":{"coresPerReplica":256,"nodesPerReplica":16,"min":2}}
Anti-affinity (spread replicas across nodes — do not let both CoreDNS pods land on same node):
# Already set by default in most clusters — verify:
kubectl get deployment coredns -n kube-system -o jsonpath='{.spec.template.spec.affinity}'
Debugging DNS
# Test from inside a pod
kubectl run dnstest --rm -it --image=busybox --restart=Never -- sh
nslookup kubernetes # should resolve
nslookup my-svc.my-namespace.svc.cluster.local
nslookup google.com # external resolution
# Check resolv.conf
kubectl exec <pod> -- cat /etc/resolv.conf
# CoreDNS logs (set log plugin in Corefile to enable query logging)
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=100
# CoreDNS metrics (if Prometheus is scraping)
# coredns_dns_requests_total
# coredns_dns_responses_total{rcode="NXDOMAIN"}
# coredns_forward_requests_duration_seconds_bucket
# Check CoreDNS pod health
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl describe pod -n kube-system -l k8s-app=kube-dns
# Validate ConfigMap syntax before applying
docker run --rm -v $(pwd)/Corefile:/Corefile coredns/coredns:1.11.1 -conf /Corefile -dryrun
Common Failure Modes
| Symptom | Cause | Fix |
|---|---|---|
| 5s timeout on DNS then success | UDP packet dropped, resolver retries after 5s timeout | Check conntrack table full: sysctl net.netfilter.nf_conntrack_max; use TCP for DNS |
| NXDOMAIN storm | App retrying DNS in tight loop on failure | Fix app to cache NXDOMAIN, use exponential backoff |
| CoreDNS OOMKilled | Too many cached entries or high QPS | Increase memory limit; reduce cache size; scale replicas |
dial tcp: lookup X: no such host |
Service doesn't exist, or wrong namespace in FQDN | `kubectl get svc -A |
| DNS works locally, fails in pod | NetworkPolicy blocks UDP/TCP 53 to kube-dns | Add egress rule: allow UDP 53 to kube-system namespace |
conntrack full → 5s DNS timeout — this is the most insidious:
# On the node:
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
# If count ≈ max: table full, new UDP flows dropped → 5s timeout
# Fix: increase nf_conntrack_max
sysctl -w net.netfilter.nf_conntrack_max=1048576
# Or: use nodeLocalDNS cache (runs on every node, uses link-local IP, bypasses conntrack)
NodeLocal DNSCache — the best fix for DNS at scale:
Each node runs a DNS cache DaemonSet on a link-local IP (169.254.20.10).
Pods resolve to this local cache first (no conntrack, no network hop).
Cache misses forward to CoreDNS. Reduces CoreDNS load by ~60-80%.