Setup CoreDNS in Docker container:
Create docker compose:
services:
coredns:
image: coredns/coredns:latest
container_name: coredns
command: -conf /etc/coredns/Corefile -dns.port 53
ports:
- "53:53"
- "53:53/udp"
- "8080:8080"
volumes:
- ./corefile/Corefile:/etc/coredns/Corefile
- ./coredns-zones:/etc/coredns/zones:ro
restart: unless-stopped
Create Corefile in corefile folder, this folder configures the coreDNS server:
. {
errors
health
prometheus
log
forward . 1.1.1.1 {
except <add dns zones: eg name.domain.com>
}
}
name.domain.com:53 {
file /etc/coredns/zones/db.name.domain.com
}
create zone files in the coredns-zones folder, these files contain the DNS records for each dns zone:
$TTL 3600
$ORIGIN yourdomain.internal.
@ IN SOA ns1.yourdomain.internal. admin.yourdomain.internal. (
2025021501 3600 1800 604800 86400 )
@ IN NS ns1.yourdomain.internal.
@ IN A 192.168.1.10
router IN A 192.168.1.1
pihole IN A 192.168.1.55
nas IN A 192.168.1.20
printer IN A 192.168.1.30
www IN CNAME server.yourdomain.internal.
Full example of Corefile:
# =====================================================================
# Example CoreDNS Corefile for DNS
#
# Purpose:
# - Serve authoritative DNS for your internal domains (e.g. lab.internal, home.arpa)
# - Forward everything else to public resolvers (with optional exceptions)
# - Basic observability, logging, and health checks
#
# Recommended file location: /etc/coredns/Corefile
# =====================================================================
# Global settings that apply to all zones
{
# Automatically reload config/zone files when they change on disk
# Very useful during testing or when updating records
reload 30s 5s
# Log all queries (useful for debugging; comment out in production)
log
# Show health endpoint (/health) and Prometheus metrics (/metrics)
health
prometheus
# Handle errors nicely
errors
# Optional: cache responses to improve performance
cache 30
}
# =====================================================================
# Catch-all / default forwarding for everything not explicitly defined
# =====================================================================
. {
# Forward non-internal queries to public resolvers
forward . 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 {
# Do NOT forward these domains — handle them authoritatively below
except internal.example.com home.arpa lan home
}
# Optional: block ads/trackers at DNS level (requires a hosts file or blocklist plugin)
# hosts /etc/coredns/blocklist.hosts {
# fallthrough
# }
}
# =====================================================================
# Authoritative zones (your internal domains)
# Use the "file" plugin to load classic BIND-style zone files
# =====================================================================
# Main internal domain
internal.example.com:53 {
file /etc/coredns/zones/db.internal.example.com
# Optional: allow zone transfers to known secondaries (rarely needed in small setups)
# transfer {
# to 192.168.50.20
# }
}
# Reverse DNS for your LAN subnet (example: 192.168.50.0/24)
50.168.192.in-addr.arpa:53 {
file /etc/coredns/zones/db.50.168.192.in-addr.arpa
}
# Another internal domain (e.g. for IoT devices)
iot.home.arpa:53 {
file /etc/coredns/zones/db.iot.home.arpa
}
# Short local domain (very common in homelabs)
lan:53 {
file /etc/coredns/zones/db.lan
}
# =====================================================================
# Optional: wildcard / catch-all for testing or dynamic environments
# =====================================================================
# *.internal.example.com:53 {
# file /etc/coredns/zones/db.wildcard.internal.example.com
# }
# =====================================================================
# Optional: mDNS / .local support (multicast DNS) - only if needed
# =====================================================================
# local:53 {
# mdns
# forward . 224.0.0.251:5353 # multicast address
# }
Full example zone file:
; =====================================================================
; Example CoreDNS zone file (BIND/RFC 1035 style)
; Used with the "file" plugin in CoreDNS
;
; File name convention: db.<zonename> (e.g. db.internal.example.com)
; Place in: /etc/coredns/zones/ or similar mounted directory
;
; Best practice: keep one zone file per domain/zone
; =====================================================================
$TTL 3600 ; default TTL for records without explicit TTL (1 hour)
$ORIGIN internal.example.com. ; base domain - all unqualified names are relative to this
; =====================================================================
; SOA Record (required for a proper authoritative zone)
; =====================================================================
@ IN SOA ns1.internal.example.com. hostmaster.internal.example.com. (
2026021501 ; serial number YYYYMMDDnn format - increment on every change
3600 ; refresh how often secondary servers check for updates
1800 ; retry how long to wait before retrying failed refresh
604800 ; expire when secondary servers should stop serving zone
86400 ) ; minimum / negative cache TTL
; =====================================================================
; NS Records (nameservers for this zone)
; =====================================================================
@ IN NS ns1.internal.example.com.
@ IN NS ns2.internal.example.com. ; optional second NS
; =====================================================================
; Common records - apex / root of zone
; =====================================================================
@ IN A 192.168.50.10 ; main server / router / traefik ingress
@ IN AAAA fd12:3456::10 ; IPv6 example (optional)
; =====================================================================
; A / AAAA records (most common)
; =====================================================================
router IN A 192.168.50.1
printer IN A 192.168.50.20
nas IN A 192.168.50.30
nas IN AAAA fd12:3456::30
pihole IN A 192.168.50.40
homeassistant IN A 192.168.50.50
proxmox1 IN A 192.168.50.60
proxmox2 IN A 192.168.50.61
; Short aliases / CNAMEs
www IN CNAME traefik.internal.example.com.
dashboard IN CNAME traefik.internal.example.com.
grafana IN CNAME traefik.internal.example.com.
; =====================================================================
; MX record (if you run internal mail)
; =====================================================================
@ IN MX 10 mail.internal.example.com.
mail IN A 192.168.50.70
; =====================================================================
; TXT records (SPF, verification, etc.)
; =====================================================================
@ IN TXT "v=spf1 a mx ~all"
_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@internal.example.com;"
; =====================================================================
; SRV records (useful for discovery - e.g. Home Assistant, mDNS-like, SIP, etc.)
; =====================================================================
; Example: _http._tcp SRV for web services
_http._tcp IN SRV 0 5 80 traefik.internal.example.com.
; Example: Home Assistant companion app discovery
_hassio._tcp IN SRV 0 0 8123 homeassistant.internal.example.com.
; =====================================================================
; Wildcard example (catch-all for undefined subdomains)
; Be careful - only use if you really want this behavior
; =====================================================================
; * IN A 192.168.50.10 ; everything → traefik ingress
; =====================================================================
; Reverse DNS / PTR records belong in separate in-addr.arpa zones
; Example for 192.168.50.0/24 → you would create db.50.168.192.in-addr.arpa
; =====================================================================
; 10 IN PTR router.internal.example.com.
; 20 IN PTR printer.internal.example.com.
; etc.
; =====================================================================