CoreDNS¶
Who this page is for: in plain English, CoreDNS is the DNS server inside your cluster - it's what turns a Service name like api.backend into an IP address. This page is production depth, aimed at readers already comfortable with Pods, Services, and Deployments. New to Kubernetes? Start with the Beginner track and DNS and Service Discovery first.
CoreDNS is a CNCF graduated DNS server written in Go, and it has been the default cluster DNS provider for Kubernetes since it replaced kube-dns in 1.13 (GA in 1.11, default in 1.13). Every Service and Pod DNS name in a cluster - db, api.backend, kafka-0.kafka.events.svc.cluster.local - is answered by CoreDNS pods running in kube-system.
What makes CoreDNS interesting beyond "it answers DNS queries" is its architecture: it isn't a monolithic resolver with a config file full of options, it's a plugin chain. Each plugin does exactly one job - serve cluster records, cache, forward upstream, log - and you compose them into a pipeline per DNS zone. That plugin model is also what makes CoreDNS usable well beyond Kubernetes, which is a big part of why it graduated in the CNCF alongside Kubernetes itself, Prometheus, and Envoy.
This page assumes you already know the basics of Kubernetes DNS - Service DNS names, the search path, ndots:5. Here we go deeper into CoreDNS itself: the Corefile syntax, the plugin chain, NodeLocal DNSCache, scaling, and troubleshooting a broken resolver.
Architecture¶
flowchart TD
subgraph Pod
RC["/etc/resolv.conf\nnameserver 10.96.0.10"]
end
RC --> SVC["kube-dns Service\nClusterIP"]
SVC --> C1[CoreDNS pod 1]
SVC --> C2[CoreDNS pod 2]
subgraph "CoreDNS plugin chain (per zone)"
direction LR
ERR[errors] --> HEALTH[health] --> READY[ready] --> K8S["kubernetes\n(cluster.local)"] --> FWD["forward\n(everything else)"] --> CACHE[cache]
end
C1 --> ERR
K8S -->|watches| API["kube-apiserver\nServices + EndpointSlices"]
FWD --> UP["Upstream resolvers\n(/etc/resolv.conf on node,\nor VPC/on-prem DNS)"]
CoreDNS runs as a Deployment (2 replicas by default) in kube-system, fronted by the kube-dns Service at a fixed ClusterIP (commonly 10.96.0.10). The kubelet writes that IP into every pod's /etc/resolv.conf - that's the entire integration point between kubelet and DNS.
Internally, each CoreDNS instance keeps an in-memory, eventually-consistent view of cluster state by watching Services and EndpointSlices through the kubernetes plugin. It never queries the API server per-lookup - it answers from the watch cache, which is why CoreDNS scales well with query volume but must reconnect and resync if it loses its API server watch.
The Corefile and the plugin chain model¶
Configuration lives in the coredns ConfigMap in kube-system, in a Corefile:
.: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
}
Each top-level block (.:53 { ... }) defines a server block - a zone (. means "everything") plus a port, and inside it, an ordered plugin chain. Every query that hits this server block flows through the plugins in the order they're listed, and each plugin can:
- answer the query and stop the chain,
- decline and pass to the next plugin,
- or (for a few plugins) explicitly
fallthroughto a later plugin even after partially handling the zone.
This ordering is the single most important thing to understand about a Corefile - moving cache before kubernetes, for example, would cache negative/incomplete answers in ways you don't want.
Plugins that matter in a cluster context¶
| Plugin | Job |
|---|---|
errors |
logs errors to stdout |
health |
exposes /health on :8080 for the kubelet liveness probe |
ready |
exposes /ready on :8181; only reports ready once the kubernetes plugin has synced its watch |
kubernetes |
the core plugin - answers cluster.local (and reverse in-addr.arpa/ip6.arpa) zones from the Service/EndpointSlice watch cache |
forward |
forwards anything the kubernetes plugin didn't claim to upstream resolvers - by default, the node's own /etc/resolv.conf |
cache |
positive/negative response caching, TTL-bounded (cache 30 = 30s max) |
loop |
detects a forwarding loop (CoreDNS forwarding to itself) and crashes the process on purpose so Kubernetes restarts it and surfaces the failure loudly |
autopath |
short-circuits the client-side search-path retries server-side, so a pod doesn't have to make 3-4 round trips for one name (opt-in, adds server-side CPU cost) |
log |
per-query access logging - noisy, enable only for active debugging |
reload |
watches the Corefile and reloads config without a pod restart when the ConfigMap changes |
prometheus |
exposes CoreDNS's own metrics for scraping, separate from cluster-workload metrics |
The kubernetes plugin's fallthrough in-addr.arpa ip6.arpa line matters: without it, any reverse-lookup query for an IP CoreDNS doesn't recognize as a pod/service IP would get an authoritative NXDOMAIN instead of being forwarded upstream.
How Service and Pod DNS records are generated¶
CoreDNS doesn't store a zone file - it synthesizes records on the fly from the Service/EndpointSlice objects it's watching:
| Object | Query pattern | Answer |
|---|---|---|
| ClusterIP Service | <svc>.<ns>.svc.cluster.local A/AAAA |
the Service's ClusterIP |
| Headless Service | <svc>.<ns>.svc.cluster.local A/AAAA |
one record per ready pod IP - returned directly, no VIP involved |
| StatefulSet pod (via headless Service) | <pod>.<svc>.<ns>.svc.cluster.local A/AAAA |
that specific pod's IP |
| Named Service port | _<port-name>._<proto>.<svc>.<ns>.svc.cluster.local SRV |
port number + target hostname |
Pod (opt-in, subdomain + hostname set) |
<hostname>.<subdomain>.<ns>.svc.cluster.local |
pod IP |
Headless Services (clusterIP: None) are the mechanism client-side load balancers and peer-discovery code rely on - a normal query for the Service name returns every ready pod's IP as a separate A record, and the client picks. This is also how StatefulSet pods get individually addressable names: the governing headless Service is what makes <pod>.<svc> resolvable at all.
Because these records are generated from live watch state, an unready pod simply doesn't appear in the answer. "DNS returns nothing" is very often "no pod is ready" rather than a CoreDNS problem - confirm readiness before you suspect the resolver.
The ndots:5 trap, from CoreDNS's side¶
DNS and Service Discovery covers ndots:5 and the search path from the client (pod) perspective in detail - read that first if you haven't. The CoreDNS-side consequence is worth stating plainly: every external, non-fully-qualified lookup from a default-configured pod arrives at CoreDNS as up to four separate queries (three search-suffix attempts that dead-end in NXDOMAIN, then the bare name). At any real request volume, that's a 4x multiplier on CoreDNS query load that has nothing to do with genuine DNS demand - it's pure search-path noise.
Two mitigations live upstream of CoreDNS (trailing dots, per-pod ndots tuning) and are covered on the DNS page. The mitigation that lives in CoreDNS's own architecture is NodeLocal DNSCache, below.
NodeLocal DNSCache¶
NodeLocal DNSCache runs a caching CoreDNS instance as a DaemonSet, one pod per node, listening on a link-local IP (169.254.20.10 by convention) bound via a dummy network interface. It sits between every pod on that node and the cluster CoreDNS Deployment.
flowchart LR
POD[Pod] -->|"query 169.254.20.10\n(via iptables, no DNAT hop)"| NLC[node-local-dns\nDaemonSet pod, this node]
NLC -->|cache miss: cluster names| SVC[kube-dns Service] --> CD[CoreDNS Deployment]
NLC -->|cache miss: external names| UP[Upstream resolvers]
Why it helps, concretely:
- Removes the ClusterIP hop for most queries. Normally every DNS query is a DNAT through the Service ClusterIP, which means a conntrack table entry. At high query rates, that's a lot of conntrack churn per second, and conntrack races are the classic cause of intermittent DNS timeouts on busy nodes.
- Absorbs repeat queries locally. A cache hit on-node never leaves the node - no network hop, no load on the central CoreDNS Deployment, no conntrack entry at all.
- Upgrades UDP to TCP for the upstream hop where useful, avoiding some of the UDP packet-drop/timeout behavior that produces the classic "every external lookup randomly takes 5 seconds" symptom.
- Survives central CoreDNS being briefly unavailable for already-cached names, since each node keeps its own cache.
It's installed via a DaemonSet manifest (templated from the official nodelocaldns.yaml) plus a small kubelet-level DNS config change (or a CNI plugin that handles the redirect), and is standard equipment on any cluster with meaningful DNS query volume. Managed offerings increasingly ship an equivalent by default rather than leaving it opt-in: GKE Autopilot runs NodeLocal DNSCache by default and doesn't let you disable it, GKE Standard enables it by default on recent versions but allows disabling it, and EKS Auto Mode includes it automatically as part of its managed cluster DNS. AKS takes a different path - LocalDNS, a separate AKS-specific per-node DNS proxy (not the upstream nodelocaldns.yaml project, though functionally similar and also listening on a link-local address, 169.254.10.10/.11) - which AKS auto-enables on node pools running recent Kubernetes versions (with some exclusions, e.g. clusters already running upstream NodeLocal DNSCache) and preconfigures by default on AKS Automatic clusters. Because the managed defaults here move quickly and differ per provider, confirm the current behaviour in your provider's own docs rather than treating the summary above as a version contract. Running both the upstream project and AKS's LocalDNS on the same node pool is explicitly discouraged.
Scaling CoreDNS¶
CoreDNS itself is stateless and horizontally scalable - more replicas just spreads query load, since each pod independently watches the same API state. Two supported approaches:
cluster-proportional-autoscaler - the traditional approach; a small controller watches the cluster's node/core count and scales the CoreDNS Deployment's replica count using a linear formula (replicas = ceil(cores / coresPerReplica), floored by min). This tracks cluster size, not query rate, which is a reasonable proxy since larger clusters generally mean more pods generating more queries.
# ConfigMap consumed by the cluster-proportional-autoscaler Deployment
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-autoscaler
namespace: kube-system
data:
linear: |-
{
"coresPerReplica": 256,
"nodesPerReplica": 16,
"min": 2,
"max": 10,
"preventSinglePointFailure": true
}
HPA on CPU/memory - works but is a weaker signal for CoreDNS specifically, since query-driven CPU spikes can be sharp and short-lived; HPA's reaction time may lag the spike. If you run HPA on CoreDNS, keep minReplicas at 2+ regardless (preventSinglePointFailure-equivalent), because a single CoreDNS pod restarting is a cluster-wide DNS blip.
In practice: NodeLocal DNSCache reduces how much either scaling mechanism matters, because most query volume never reaches the central Deployment at all.
Custom stub domains and upstream forwarding¶
Hybrid and on-prem clusters commonly need CoreDNS to route specific internal zones to a different resolver than the general internet-facing forward. Add a second server block scoped to that zone:
consul.local:53 {
errors
cache 30
forward . 10.150.0.10 10.150.0.11
}
corp.internal:53 {
errors
cache 30
forward . 10.0.0.53 {
tls_servername dns.corp.internal
}
}
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
CoreDNS routes a query to the most specific matching server block, so foo.consul.local goes to the Consul resolvers and everything else falls through to the default . block. This is the standard pattern for split-horizon DNS: internal service-discovery systems (Consul, on-prem AD-integrated DNS) get their own dedicated forward target instead of being mixed into the general upstream list.
Editing the Corefile directly via kubectl edit configmap coredns -n kube-system is fine for a quick test, but manage it as code in the long run (Helm value, Kustomize patch, or GitOps-applied manifest) - it's cluster-critical config that deserves review and diffing like anything else.
Troubleshooting DNS resolution failures¶
A systematic path, from "is it DNS" down to "is it this specific CoreDNS instance":
# 1. Confirm it's actually a DNS problem
kubectl run dnstest --rm -it --image=busybox:1.36 --restart=Never -- nslookup api.backend
# 2. Check CoreDNS pod health and restart counts
kubectl get pods -n kube-system -l k8s-app=kube-dns
# 3. Read CoreDNS's own logs
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=100
# 4. Confirm the Corefile in the cluster matches what you expect
kubectl get configmap coredns -n kube-system -o jsonpath='{.data.Corefile}'
# 5. Check CoreDNS's Prometheus metrics for error/latency spikes
kubectl exec -n kube-system <coredns-pod> -- wget -qO- localhost:9153/metrics | grep coredns_dns
# 6. Confirm the pod can actually reach the DNS ClusterIP (NetworkPolicy is a frequent culprit)
kubectl exec -it <pod> -- nc -zu 10.96.0.10 53
CoreDNS in CrashLoopBackOff from loop detection is the most common self-inflicted CoreDNS outage, and it's worth knowing exactly why it happens: if the node's /etc/resolv.conf points at 127.0.0.1 (a local systemd-resolved stub, common on Ubuntu) and CoreDNS's forward plugin naively forwards to that address from inside a pod network namespace, the query can loop back to CoreDNS itself. The loop plugin detects this and deliberately crashes the process rather than spin forever - that's a feature, not a bug, but it means the actual fix is upstream: point forward at a real, non-looping resolver (the node's real upstream DNS servers, or a cloud provider's VPC resolver), not at the node's local stub. This is a known interaction on kubeadm-provisioned nodes running systemd-resolved; the standard fix is templating forward . <explicit-upstream-ips> instead of forward . /etc/resolv.conf, or fixing the node's resolv.conf to skip the stub resolver.
Other common causes: NetworkPolicy blocking UDP/TCP 53 egress to kube-system after policies are introduced; CoreDNS pods failing their ready check because the API server watch hasn't synced (check for API server connectivity issues); and upstream i/o timeout in logs, which points at the external resolver, not CoreDNS.
Common mistakes¶
- Editing the Corefile without understanding chain order - inserting
cachebeforekubernetes, or removingfallthrough, silently breaks reverse lookups or caches wrong answers. - Forwarding to
/etc/resolv.confon kubeadm nodes runningsystemd-resolved- the classicloop-detected crash loop described above. - Running a single CoreDNS replica - makes DNS a single point of failure for the entire cluster; always run at least 2, spread across nodes with pod anti-affinity.
- Skipping NodeLocal DNSCache on large or DNS-heavy clusters - and then chasing intermittent 5-second latency spikes that NodeLocal DNSCache would have absorbed.
- Forgetting the egress NetworkPolicy rule for port 53 the moment default-deny policies are introduced - every namespace needs an explicit allow for DNS or nothing resolves.
- Enabling
login production and leaving it on - per-query logging at scale generates enormous log volume for little ongoing benefit; enable it temporarily for debugging, then remove it.
Certification notes¶
- CoreDNS is explicit CKA material: know that it lives in
kube-system, is configured via thecorednsConfigMap Corefile, and is exposed by thekube-dnsService. - Exam-style troubleshooting tasks often plant a broken Corefile (wrong forward target, missing zone) or a CrashLoopBackOff CoreDNS pod -
kubectl logsandkubectl get configmap coredns -n kube-system -o yamlare your first two commands. - For CKS: know that locking down egress with NetworkPolicy requires an explicit allow rule for DNS, and that CoreDNS's own pod should be covered by your cluster's RBAC and PSA baseline like any other workload.
Source Links¶
- CoreDNS documentation and plugin reference
- coredns/coredns on GitHub and its releases
- The
kubernetesplugin - The
cache,forward, andautopathplugins - Kubernetes: DNS for Services and Pods
- Kubernetes: customizing DNS service
- Kubernetes: NodeLocal DNSCache
- GKE: NodeLocal DNSCache
- AKS: DNS in Azure Kubernetes Service
- CNCF project page: CoreDNS
Related Concepts¶
- DNS and Service Discovery - Service/Pod DNS records, the search path, and
ndots:5from the client side - Services - what the ClusterIP CoreDNS answers actually points at
- Networking Overview
- Network Policies - the DNS egress rule every namespace needs
- kubelet and Container Runtime - who writes each pod's resolv.conf
- Troubleshooting - the wider debugging flow when name resolution is only a symptom
- CKA Exam Guide - CoreDNS debugging is an exam objective