How to Self-Host CoreDNS with Docker Compose
Docker Compose Configuration
CoreDNS is the DNS server that powers Kubernetes — but it works just as well as a standalone resolver for your homelab. A single static binary, configured with a text file called a Corefile, it handles forwarding, caching, zone serving, and service discovery through a plugin chain architecture.
Unlike Unbound (which focuses on recursive resolution) or Pi-hole (which focuses on ad blocking), CoreDNS is a modular building block. You compose behavior from plugins: forward for upstream resolution, cache for caching, file for zone serving, hosts for local overrides. This makes it flexible but requires more configuration upfront.
Create a directory for your CoreDNS deployment:
mkdir -p /opt/coredns && cd /opt/coredns
Create a docker-compose.yml file:
services:
coredns:
image: coredns/coredns:1.14.1
container_name: coredns
command: -conf /etc/coredns/Corefile
ports:
- "53:53/tcp"
- "53:53/udp"
volumes:
- ./Corefile:/etc/coredns/Corefile:ro
- ./zones:/etc/coredns/zones:ro
restart: unless-stopped
Note on health checks: The official CoreDNS image uses a distroless base (no shell, no curl, no wget). Docker health checks using
CMD-SHELLwon’t work. CoreDNS exposes a health endpoint athttp://localhost:8080/health, but you’ll need external monitoring to check it. The process-levelrestart: unless-stoppedhandles crash recovery.
What Is CoreDNS?
CoreDNS started as a fork of Caddy web server’s plugin architecture, applied to DNS. Since 2018, it’s been the default DNS server in Kubernetes clusters. It processes DNS queries through an ordered chain of plugins — each plugin can modify the query, generate a response, or pass to the next plugin. This makes CoreDNS uniquely flexible: the same server can forward queries upstream, serve authoritative zones, rewrite queries, and cache results.
CoreDNS is maintained by the CNCF and has a strong open-source community. It supports DNS over UDP, TCP, TLS (DoT), HTTPS (DoH), and gRPC.
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 64 MB of free RAM (CoreDNS is extremely lightweight)
- Port 53 available (disable systemd-resolved if needed)
- Understanding of DNS basics (DNS Explained)
Free Up Port 53
sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
Corefile Configuration
The Corefile is CoreDNS’s entire configuration. Create it at /opt/coredns/Corefile:
# Health check endpoint
(common) {
log
errors
cache 3600
}
# Forward all queries to upstream DNS
. {
import common
forward . 1.1.1.1 9.9.9.9
health localhost:8080
ready localhost:8181
}
This minimal config forwards all DNS queries to Cloudflare and Quad9, with caching and logging enabled.
Serving Local Zones
For custom DNS entries on your network, create a zone file:
mkdir -p /opt/coredns/zones
Add a local zone to your Corefile:
# Local homelab zone
home.lan {
file /etc/coredns/zones/db.home.lan
log
}
# Forward everything else upstream
. {
forward . 1.1.1.1 9.9.9.9
cache 3600
health localhost:8080
log
errors
}
Create /opt/coredns/zones/db.home.lan:
$ORIGIN home.lan.
@ 3600 IN SOA ns1.home.lan. admin.home.lan. (
2026030401 ; serial
7200 ; refresh
3600 ; retry
1209600 ; expire
3600 ; minimum
)
@ IN NS ns1.home.lan.
ns1 IN A 192.168.1.10
; Homelab services
nas IN A 192.168.1.100
proxmox IN A 192.168.1.50
docker IN A 192.168.1.10
jellyfin IN A 192.168.1.10
nextcloud IN A 192.168.1.10
pihole IN A 192.168.1.10
Start the stack:
docker compose up -d
Initial Setup
Test DNS resolution:
# Forward query test
dig @127.0.0.1 example.com
# Local zone test (if configured)
dig @127.0.0.1 nas.home.lan
# Health check
curl http://localhost:8080/health
Configuration
Plugin Reference
CoreDNS configuration is plugin-driven. Here are the most useful plugins for self-hosting:
| Plugin | Purpose | Example |
|---|---|---|
forward | Forward queries to upstream DNS | forward . 1.1.1.1 9.9.9.9 |
cache | Cache DNS responses | cache 3600 (TTL in seconds) |
file | Serve authoritative zones from files | file /etc/coredns/zones/db.example.com |
hosts | Serve entries from a hosts-format file | hosts /etc/coredns/custom.hosts |
log | Log queries to stdout | log |
errors | Log errors to stdout | errors |
health | HTTP health check endpoint | health localhost:8080 |
ready | Readiness probe endpoint | ready localhost:8181 |
rewrite | Rewrite queries before processing | rewrite name foo.old.lan foo.new.lan |
template | Generate responses from templates | Template-based dynamic responses |
acl | Access control lists | acl { allow net 192.168.0.0/16 } |
tls | Enable DNS-over-TLS | tls /certs/cert.pem /certs/key.pem |
Forwarding With TLS (DNS-over-TLS)
. {
forward . tls://1.1.1.1 tls://1.0.0.1 {
tls_servername cloudflare-dns.com
health_check 5s
}
cache 3600
log
}
Multiple Upstream Servers With Policy
. {
forward . 1.1.1.1 9.9.9.9 8.8.8.8 {
policy round_robin # or random, sequential
health_check 10s
}
cache 3600
}
Hosts File Override
For simple local DNS without full zone files:
cat > /opt/coredns/custom.hosts << 'EOF'
192.168.1.100 nas.home.lan
192.168.1.50 proxmox.home.lan
192.168.1.10 docker.home.lan
EOF
. {
hosts /etc/coredns/custom.hosts {
fallthrough
}
forward . 1.1.1.1
cache 3600
log
}
The fallthrough directive passes unmatched queries to the next plugin (forward).
Advanced Configuration
Split DNS
Route different domains to different resolvers — useful when you have internal services alongside public DNS:
# Internal corporate DNS
corp.example.com {
forward . 10.0.0.53
cache 600
}
# VPN-accessible services
internal.home {
file /etc/coredns/zones/db.internal.home
}
# Everything else → public DNS
. {
forward . 1.1.1.1 9.9.9.9
cache 3600
health localhost:8080
log
}
See Split DNS Setup for detailed patterns.
DNS-over-HTTPS (DoH) Server
CoreDNS can serve DoH to browsers and devices that support it:
https://.:443 {
tls /certs/fullchain.pem /certs/privkey.pem
forward . 1.1.1.1
cache 3600
log
}
Add port 443 to your Docker Compose port mappings.
Reverse Proxy
CoreDNS primarily serves DNS on port 53 — a standard reverse proxy isn’t needed. If you’re exposing the health/ready endpoints or DoH externally, route through Nginx Proxy Manager or Caddy. For remote DNS access, use Tailscale or WireGuard.
Backup
CoreDNS stores no persistent state — all configuration lives in files:
- Corefile — the main configuration
- Zone files — any zones in
/opt/coredns/zones/ - Hosts file — if using the hosts plugin
tar czf coredns-backup-$(date +%F).tar.gz /opt/coredns/
The DNS cache exists only in memory and rebuilds on restart. See Backup Strategy for general guidance.
Troubleshooting
Container Exits Immediately
Symptom: CoreDNS container starts and stops within seconds
Fix: Check for Corefile syntax errors:
docker compose logs coredns
Common issues: missing closing braces, invalid plugin names, wrong indentation. The Corefile is whitespace-sensitive — plugins must be indented inside server blocks.
Port 53 Already in Use
Symptom: bind: address already in use
Fix: Same as any DNS server — disable systemd-resolved:
sudo systemctl disable --now systemd-resolved
Zone File Not Loading
Symptom: Local zone queries return NXDOMAIN
Fix: Verify the zone file path inside the container matches the Corefile reference. Check that the volume mount maps correctly and the file is readable. Zone files must include a valid SOA record.
Forward Plugin Timeout
Symptom: Queries take 5+ seconds or fail intermittently
Fix: Add health checking to the forward plugin:
forward . 1.1.1.1 9.9.9.9 {
health_check 5s
}
This removes unresponsive upstreams from the rotation automatically.
Resource Requirements
| Resource | Value |
|---|---|
| RAM | ~15 MB idle, ~30 MB under moderate load |
| CPU | Negligible — single static binary |
| Disk | ~50 MB (container image) + config files |
| Network | Minimal; outbound to upstream DNS servers |
CoreDNS is one of the lightest DNS servers available. It runs comfortably on a Raspberry Pi.
Verdict
CoreDNS wins on flexibility and resource efficiency. If you want a DNS server that does exactly what you configure — no more, no less — with a clean plugin architecture, CoreDNS delivers. It’s the right choice for homelabs that need split DNS, custom zone serving, or DNS-over-HTTPS, and for anyone comfortable with text-based configuration.
For users who want recursive DNSSEC-validating resolution without configuring plugins, Unbound is more turnkey. For DNS-level ad blocking with a web UI, Pi-hole or AdGuard Home is what you want. CoreDNS doesn’t filter ads — it’s a resolver and zone server, not a sinkhole.
FAQ
Is CoreDNS the same as what runs in Kubernetes?
Yes — CoreDNS has been the default Kubernetes DNS since 2018. The standalone Docker image is the same binary, just configured via Corefile instead of a Kubernetes ConfigMap.
Can CoreDNS block ads like Pi-hole?
Not out of the box. CoreDNS has no built-in blocklist mechanism. You could use the hosts plugin with a blocklist file, but it’s not designed for this. Use Pi-hole or AdGuard Home for ad blocking.
Should I use CoreDNS or Unbound?
CoreDNS if you need modular, plugin-driven DNS with zone serving and split DNS. Unbound if you want a dedicated recursive resolver with DNSSEC and minimal configuration. See Unbound vs CoreDNS for a detailed comparison.
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments