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-SHELL won’t work. CoreDNS exposes a health endpoint at http://localhost:8080/health, but you’ll need external monitoring to check it. The process-level restart: unless-stopped handles 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:

PluginPurposeExample
forwardForward queries to upstream DNSforward . 1.1.1.1 9.9.9.9
cacheCache DNS responsescache 3600 (TTL in seconds)
fileServe authoritative zones from filesfile /etc/coredns/zones/db.example.com
hostsServe entries from a hosts-format filehosts /etc/coredns/custom.hosts
logLog queries to stdoutlog
errorsLog errors to stdouterrors
healthHTTP health check endpointhealth localhost:8080
readyReadiness probe endpointready localhost:8181
rewriteRewrite queries before processingrewrite name foo.old.lan foo.new.lan
templateGenerate responses from templatesTemplate-based dynamic responses
aclAccess control listsacl { allow net 192.168.0.0/16 }
tlsEnable DNS-over-TLStls /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

ResourceValue
RAM~15 MB idle, ~30 MB under moderate load
CPUNegligible — single static binary
Disk~50 MB (container image) + config files
NetworkMinimal; 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.

Comments