DNS for System Administrators: Complete Guide to Domain Name Resolution
DNS is the invisible infrastructure that translates human-readable domain names into machine-routable IP addresses. It is so fundamental that when DNS breaks, the internet stops working. Every system administrator must understand DNS, not just conceptually, but operationally: how to configure records, run local resolvers, troubleshoot resolution failures, and secure DNS queries.
This guide covers DNS from the protocol level through to running your own DNS infrastructure for a homelab or production environment.
The DNS Hierarchy
DNS is a distributed, hierarchical database. When you type grafana.example.com into a browser, the resolution follows a chain of authoritative servers:
- Root servers (.): 13 logical root server groups (a.root-servers.net through m.root-servers.net) that know where to find TLD servers. There are over 1,500 physical instances distributed globally via anycast.
- TLD servers (.com, .org, .net): Know which nameservers are authoritative for each domain under their TLD.
- Authoritative nameservers (example.com): Hold the actual DNS records for a domain. These are typically operated by your domain registrar or a DNS provider like Cloudflare.
- Recursive resolver: Your ISP or a public resolver (1.1.1.1, 8.8.8.8) that walks the hierarchy on your behalf and caches results.
Caching is critical. Without caching, every DNS query would require multiple round trips to root, TLD, and authoritative servers. TTL (Time To Live) values on DNS records control how long resolvers cache each response. A TTL of 3600 means the record is cached for one hour.
DNS Record Types
| Record Type | Purpose | Example |
|---|---|---|
| A | Maps hostname to IPv4 address | server.example.com. 3600 IN A 93.184.216.34 |
| AAAA | Maps hostname to IPv6 address | server.example.com. 3600 IN AAAA 2606:2800:220:1:: |
| CNAME | Alias one name to another | www.example.com. 3600 IN CNAME example.com. |
| MX | Mail server for a domain | example.com. 3600 IN MX 10 mail.example.com. |
| TXT | Arbitrary text (SPF, DKIM, verification) | example.com. 3600 IN TXT "v=spf1 include:_spf.google.com ~all" |
| SRV | Service location (port + priority) | _sip._tcp.example.com. 3600 IN SRV 10 5 5060 sip.example.com. |
| NS | Authoritative nameserver for a zone | example.com. 86400 IN NS ns1.example.com. |
| PTR | Reverse DNS (IP to hostname) | 34.216.184.93.in-addr.arpa. 3600 IN PTR server.example.com. |
| SOA | Start of Authority (zone metadata) | Serial number, refresh intervals, admin contact |
| CAA | Certificate Authority Authorization | example.com. 3600 IN CAA 0 issue "letsencrypt.org" |
example.com. This is why the root domain (apex) typically uses A/AAAA records, while subdomains can use CNAMEs. Some DNS providers offer a proprietary "ALIAS" or "ANAME" record to work around this limitation.
Running Your Own DNS Server
Pi-hole: Ad Blocking + Local DNS
Pi-hole is the most popular self-hosted DNS solution. It blocks ads and trackers at the DNS level and provides local DNS resolution for your homelab services:
# Deploy Pi-hole with Docker
docker run -d \
--name pihole \
--restart unless-stopped \
-p 53:53/tcp \
-p 53:53/udp \
-p 80:80/tcp \
-e TZ='America/New_York' \
-e WEBPASSWORD='your-admin-password' \
-e PIHOLE_DNS_='1.1.1.1;1.0.0.1' \
-v pihole_data:/etc/pihole \
-v pihole_dnsmasq:/etc/dnsmasq.d \
pihole/pihole:latest
# Add local DNS records via the web UI or CLI:
# Local DNS Records > Add:
# grafana.home.lab -> 192.168.1.100
# nextcloud.home.lab -> 192.168.1.100
# usulnet.home.lab -> 192.168.1.100
# Or add via the command line:
docker exec pihole bash -c \
'echo "192.168.1.100 grafana.home.lab" >> /etc/pihole/custom.list'
docker exec pihole pihole restartdns
Unbound: Recursive Resolver
Unbound is a recursive DNS resolver that queries root servers directly instead of forwarding to a third-party resolver like Cloudflare or Google. Paired with Pi-hole, it gives you complete DNS privacy:
# /opt/docker/dns/docker-compose.yml
services:
pihole:
image: pihole/pihole:latest
container_name: pihole
restart: unless-stopped
ports:
- "53:53/tcp"
- "53:53/udp"
- "8080:80/tcp"
environment:
TZ: 'America/New_York'
WEBPASSWORD: '${PIHOLE_PASSWORD}'
PIHOLE_DNS_: '172.20.0.2#5335'
volumes:
- pihole_data:/etc/pihole
- pihole_dnsmasq:/etc/dnsmasq.d
networks:
dns:
ipv4_address: 172.20.0.3
depends_on:
- unbound
unbound:
image: mvance/unbound:latest
container_name: unbound
restart: unless-stopped
volumes:
- ./unbound.conf:/opt/unbound/etc/unbound/unbound.conf:ro
networks:
dns:
ipv4_address: 172.20.0.2
networks:
dns:
ipam:
config:
- subnet: 172.20.0.0/24
volumes:
pihole_data:
pihole_dnsmasq:
# unbound.conf - Recursive resolver configuration
server:
verbosity: 0
interface: 0.0.0.0
port: 5335
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
# Security
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
# Performance
num-threads: 2
msg-cache-size: 64m
rrset-cache-size: 128m
cache-min-ttl: 3600
cache-max-ttl: 86400
prefetch: yes
prefetch-key: yes
# Privacy
qname-minimisation: yes
aggressive-nsec: yes
# Access control
access-control: 172.20.0.0/24 allow
access-control: 127.0.0.0/8 allow
# Root hints (auto-updated)
root-hints: /opt/unbound/etc/unbound/root.hints
CoreDNS: Flexible DNS Server
CoreDNS is a lightweight, plugin-based DNS server used extensively in Kubernetes. It is excellent for custom DNS configurations:
# Corefile - CoreDNS configuration
home.lab {
hosts {
192.168.1.100 server.home.lab
192.168.1.100 grafana.home.lab
192.168.1.100 nextcloud.home.lab
192.168.1.100 usulnet.home.lab
192.168.1.101 nas.home.lab
fallthrough
}
log
errors
}
. {
forward . 1.1.1.1 1.0.0.1
cache 30
log
errors
}
Split-Horizon DNS
Split-horizon (or split-brain) DNS returns different results depending on where the query comes from. Internal clients get private IP addresses, while external clients get public addresses. This is essential for self-hosted services that are accessible both locally and remotely:
# With Pi-hole local DNS:
# Internal clients (192.168.1.0/24) resolve:
# grafana.example.com -> 192.168.1.100 (local IP)
# External clients resolve via public DNS:
# grafana.example.com -> 203.0.113.50 (public IP)
# Pi-hole handles this automatically when you add
# local DNS entries for your domains. Queries from
# your LAN get the local answer; queries from outside
# never hit Pi-hole and use your public DNS records.
# For more complex setups, use conditional forwarding
# or views in BIND:
DNSSEC
DNSSEC (DNS Security Extensions) adds cryptographic signatures to DNS records, preventing forgery and cache poisoning attacks. While you should enable DNSSEC validation in your resolver, implementing DNSSEC signing for your own domains requires careful operational commitment:
# Check if a domain has DNSSEC enabled
dig +dnssec example.com
# Look for the 'ad' flag (Authenticated Data) in the response
# ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2
# Verify DNSSEC chain
dig +trace +dnssec example.com
# Enable DNSSEC validation in Unbound (enabled by default)
# In unbound.conf:
server:
# DNSSEC validation
auto-trust-anchor-file: "/opt/unbound/etc/unbound/root.key"
val-clean-additional: yes
Troubleshooting DNS with dig and nslookup
dig is the most powerful DNS troubleshooting tool. Learn these commands and you can diagnose almost any DNS issue:
# Basic query
dig example.com
# Query for a specific record type
dig example.com MX
dig example.com TXT
dig example.com AAAA
dig example.com NS
# Query a specific DNS server
dig @1.1.1.1 example.com
dig @192.168.1.100 grafana.home.lab
# Short answer only
dig +short example.com
# Trace the full resolution path
dig +trace example.com
# Check reverse DNS
dig -x 93.184.216.34
# Check all record types
dig example.com ANY
# Query with DNSSEC verification
dig +dnssec +multi example.com
# Check SOA record (zone metadata)
dig example.com SOA
# Measure query time
dig example.com | grep "Query time"
# nslookup alternative (simpler but less detailed)
nslookup example.com
nslookup -type=MX example.com
nslookup example.com 1.1.1.1
Common DNS Problems and Solutions
| Problem | Symptom | Diagnosis | Fix |
|---|---|---|---|
| Stale cache | Old IP returned after record change | dig +trace shows correct, local resolver wrong |
Wait for TTL to expire or flush cache |
| Wrong nameserver | Queries go to wrong server | Check /etc/resolv.conf |
Fix resolver configuration |
| NXDOMAIN | Domain not found | dig +trace to find where chain breaks |
Check domain registration and NS records |
| Slow resolution | Long page load times | dig query time > 100ms |
Use a faster resolver or run a local cache |
| SERVFAIL | Server failure response | Often DNSSEC validation failure | Check DNSSEC configuration or disable validation |
# Flush DNS cache on different systems:
# systemd-resolved (Ubuntu)
sudo resolvectl flush-caches
# Pi-hole
pihole restartdns reload
# macOS
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
# Windows
ipconfig /flushdns
# Check what DNS server your system is using
resolvectl status
cat /etc/resolv.conf
/etc/docker/daemon.json and the container's --dns flag or Compose dns option.
DNS Best Practices for Self-Hosted Infrastructure
- Run a local DNS resolver. Pi-hole + Unbound gives you ad blocking, local DNS, and privacy from third-party resolvers.
- Use meaningful hostnames.
grafana.home.labis better than192.168.1.100:3000. - Set appropriate TTLs. 3600 (1 hour) for most records. Lower (300) for records that change frequently.
- Add CAA records. Restrict which Certificate Authorities can issue certificates for your domain.
- Monitor DNS resolution. A monitoring tool like Blackbox Exporter can alert you when DNS resolution fails.
- Document your DNS setup. Keep a record of all DNS entries, both public and local.
With usulnet managing your Docker infrastructure, services are accessible through a unified dashboard, but DNS remains the foundation that routes traffic to the right place. A well-configured DNS setup with Pi-hole and local records means you can access every service by name, both at home and remotely through your VPN.