
How to run your own DNS server with BIND or CoreDNS?
To run your own DNS server, install BIND 9 (sudo apt install bind9) or CoreDNS on at least two servers in different locations, write a zone file containing your SOA, NS and other records, configure the server as authoritative with recursion turned off, open port 53 on UDP and TCP, and test it with dig @your-server. Once both servers answer correctly, register them as the nameservers for your domain at your registrar, adding glue records if the nameservers live inside the domain they serve.
Running your own DNS gives you full control, no per-query costs and a deep understanding of how DNS works, but it also makes you responsible for uptime, security and patching. This guide covers when self-hosting makes sense, how to set up an authoritative server with BIND step by step, how to do the same with CoreDNS in Docker, how to add a secondary server, how to harden and monitor your setup, and how to put it live.
Should You Run Your Own DNS Server?
Before you start, be honest about the trade-offs.
Good reasons to self-host:
- Learning: Nothing teaches DNS like running it.
- Internal zones: Names for a lab, office or private network that shouldn't be on a public provider.
- Control and compliance: Keeping DNS data on infrastructure you own.
- Hidden primary: Running your own primary server that pushes zones to a managed provider, which serves the public.
- Integration: Custom automation through RFC 2136 dynamic updates or plugins.
Reasons to be careful:
- Availability: If your servers go down, your domain goes down. You need at least two, on separate networks.
- DDoS exposure: Public DNS servers attract attacks, and a small VPS can be overwhelmed easily.
- Maintenance: Security updates, monitoring and DNSSEC key management are your job.
A popular compromise is the hidden primary model: you edit zones on your own server, and one or more managed providers act as public secondaries. You keep control while their networks handle the traffic.
Authoritative vs Recursive
DNS servers do two very different jobs, and it's important not to mix them up on a public server:
| Role | What it does | Example software |
|---|---|---|
| Authoritative | Answers for zones you own, such as example.com | BIND, CoreDNS, Knot DNS, NSD, PowerDNS |
| Recursive | Looks up any name on behalf of clients and caches results | Unbound, BIND, PowerDNS Recursor, CoreDNS with forward |
This guide focuses on authoritative DNS for your domain. A public authoritative server should never perform recursion for the internet. Open resolvers are abused for amplification attacks.
What You'll Need
- Two servers with static public IP addresses, ideally with different providers or in different regions. This guide uses
203.0.113.53forns1and198.51.100.53forns2. - Ubuntu 24.04 LTS or Debian 12, although the configuration works on most Linux distributions.
- Port 53 open on UDP and TCP. TCP is required for large responses and zone transfers.
- A domain where you can change nameservers and create glue records.
Setting Up BIND 9 as an Authoritative Server
BIND is the most widely used DNS server software and is maintained by ISC.
Step 1: Install BIND
sudo apt update
sudo apt install bind9 bind9-utils
named -v
BIND 9.18.39-0ubuntu0.24.04.1-Ubuntu (Extended Support Version) <id:>
Your exact version will differ. Ubuntu and Debian ship BIND 9.18, an extended support release. Check that the service is running:
systemctl status named --no-pager
Step 2: Configure Global Options
Edit /etc/bind/named.conf.options:
options {
directory "/var/cache/bind";
// Authoritative only: never answer recursive queries
recursion no;
allow-recursion { none; };
// Answer queries for our zones from anyone
allow-query { any; };
// No zone transfers unless a zone allows them explicitly
allow-transfer { none; };
listen-on { any; };
listen-on-v6 { any; };
// Don't reveal the software version
version "not disclosed";
// Smaller responses, less amplification potential
minimal-responses yes;
};
Step 3: Write the Zone File
Create a directory for zones and the zone file /etc/bind/zones/db.example.com:
sudo mkdir -p /etc/bind/zones
$TTL 3600
@ IN SOA ns1.example.com. hostmaster.example.com. (
2026102401 ; serial (YYYYMMDDnn)
7200 ; refresh
3600 ; retry
1209600 ; expire
3600 ) ; negative caching TTL
IN NS ns1.example.com.
IN NS ns2.example.com.
IN A 203.0.113.10
IN AAAA 2001:db8::10
IN MX 10 mail.example.com.
IN TXT "v=spf1 mx -all"
IN CAA 0 issue "letsencrypt.org"
ns1 IN A 203.0.113.53
ns2 IN A 198.51.100.53
mail IN A 203.0.113.25
www IN CNAME example.com.
A few rules to remember:
- Trailing dots matter:
ns1.example.com.is fully qualified. Without the dot, BIND appends the zone name, producingns1.example.com.example.com.. - Lines starting with whitespace reuse the owner name from the line above, which here is
@, the zone apex. - The SOA email uses a dot instead of
@, sohostmaster.example.com.meanshostmaster@example.com. - Increase the serial every time you change the zone, or secondaries won't pick up the change.
Step 4: Declare the Zone
Add the zone to /etc/bind/named.conf.local:
zone "example.com" {
type primary;
file "/etc/bind/zones/db.example.com";
};
Older guides use type master;. BIND still accepts it, but primary is the current term.
Step 5: Check and Reload
Always validate before reloading:
sudo named-checkconf
sudo named-checkzone example.com /etc/bind/zones/db.example.com
zone example.com/IN: loaded serial 2026102401
OK
Then reload BIND:
sudo rndc reload
Step 6: Test It
Query the server directly:
dig @203.0.113.53 example.com A
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; ANSWER SECTION:
example.com. 3600 IN A 203.0.113.10
The aa flag shows the answer is authoritative. The warning about recursion is exactly what you want: the server refuses to act as a resolver. Confirm that by asking for a domain you don't host:
dig @203.0.113.53 example.org A | grep status
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 18233
Setting Up CoreDNS as an Authoritative Server
CoreDNS is a lightweight DNS server written in Go, best known as the DNS server inside Kubernetes. It's configured through a short Corefile and a chain of plugins, and it runs well as a single binary or container. It can serve the same zone file format as BIND.
Step 1: Create the Configuration
Create a directory with a Corefile and the zone file from the BIND section:
coredns/
├── Corefile
└── db.example.com
# coredns/Corefile
example.com {
file /etc/coredns/db.example.com
log
errors
}
. {
# Refuse queries for zones we don't host
acl {
block
}
}
The file plugin loads the zone and checks the file for changes periodically, reloading it when the SOA serial increases. The catch-all . block uses the acl plugin to block every other query, so the server doesn't answer for zones it doesn't own.
Step 2: Run CoreDNS With Docker Compose
# compose.yaml
services:
coredns:
image: coredns/coredns:1.11.3
command: ["-conf", "/etc/coredns/Corefile"]
ports:
- "53:53/udp"
- "53:53/tcp"
volumes:
- ./coredns:/etc/coredns:ro
restart: unless-stopped
docker compose up -d
docker compose logs coredns
coredns-1 | example.com.:53
coredns-1 | .:53
coredns-1 | CoreDNS-1.11.3
Port 53 Conflicts on Ubuntu
Ubuntu's systemd-resolved runs a stub resolver on 127.0.0.53:53, which can stop Docker or CoreDNS from binding to port 53 on all addresses. On a dedicated DNS server, turn off the stub listener:
sudo sed -i 's/^#\?DNSStubListener=.*/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo systemctl restart systemd-resolved
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
The last command points the server's own resolver configuration at the upstream DNS servers rather than the disabled stub. Alternatively, publish the container port on the server's public IP only, for example "203.0.113.53:53:53/udp".
Step 3: Test CoreDNS
dig @203.0.113.53 www.example.com A +noall +answer
www.example.com. 3600 IN CNAME example.com.
example.com. 3600 IN A 203.0.113.10
BIND or CoreDNS?
| Aspect | BIND 9 | CoreDNS |
|---|---|---|
| Maturity | Decades of production use | Mature, widely used in Kubernetes |
| Configuration | named.conf with many options | Short Corefile with plugins |
| Zone transfers | Full AXFR/IXFR, NOTIFY, TSIG | Supported via transfer and secondary plugins |
| DNSSEC | Full signing with dnssec-policy and automatic key rollover | On-the-fly signing with the dnssec plugin |
| Dynamic updates | RFC 2136 with fine-grained update-policy | Not supported by the file plugin |
| Footprint | Moderate | Small single binary |
| Best for | Traditional authoritative DNS, hidden primaries | Containers, simple zones, custom plugins |
If you want a traditional authoritative setup with every standard feature, use BIND. If you want something small and container-friendly, or you already know CoreDNS from Kubernetes, CoreDNS is a good fit.
Adding a Secondary Server
You need at least two nameservers. The simplest way to keep them in sync is a primary and secondary pair, with the secondary pulling the zone through a zone transfer secured with a TSIG key.
Generate a key on the primary and copy it to the secondary:
tsig-keygen -a hmac-sha256 xfr-key | sudo tee /etc/bind/xfr-key.conf
sudo chown root:bind /etc/bind/xfr-key.conf
sudo chmod 640 /etc/bind/xfr-key.conf
On the primary (ns1), allow transfers with the key and notify the secondary of changes:
include "/etc/bind/xfr-key.conf";
zone "example.com" {
type primary;
file "/etc/bind/zones/db.example.com";
allow-transfer { key xfr-key; };
also-notify { 198.51.100.53; };
notify yes;
};
On the secondary (ns2), with BIND installed and the same global options:
include "/etc/bind/xfr-key.conf";
server 203.0.113.53 {
keys { xfr-key; };
};
zone "example.com" {
type secondary;
file "/var/cache/bind/db.example.com";
primaries { 203.0.113.53; };
};
The server block makes ns2 sign its transfer requests to ns1 with the key. Reload both servers and check the secondary's logs:
sudo rndc reload
sudo journalctl -u named --since "5 minutes ago" | grep "transfer of"
transfer of 'example.com/IN' from 203.0.113.53#53: Transfer completed: 1 messages, 13 records, 431 bytes, 0.004 secs
Confirm both servers have the same serial:
dig @203.0.113.53 example.com SOA +short
dig @198.51.100.53 example.com SOA +short
If you use CoreDNS, the transfer plugin on the primary and the secondary plugin on the secondary provide the same pattern. You can also mix software, for example a BIND primary with a managed provider as the secondary.
Securing Your DNS Server
A public DNS server needs a few layers of protection.
Firewall
Allow DNS from anywhere and restrict everything else. With UFW:
sudo ufw allow 53/udp
sudo ufw allow 53/tcp
sudo ufw allow OpenSSH
sudo ufw enable
Disable Recursion and Restrict Transfers
You've already set recursion no and allow-transfer { none; } globally, with transfers allowed per zone only with the TSIG key. Test from a machine that isn't your secondary:
dig @203.0.113.53 example.com AXFR
; Transfer failed.
Response Rate Limiting
BIND can limit identical responses to the same client network, which reduces your usefulness in amplification attacks. Add this to the options block:
rate-limit {
responses-per-second 10;
window 5;
};
DNSSEC Signing
BIND can sign your zone and manage keys automatically with dnssec-policy. In the zone block:
zone "example.com" {
type primary;
file "/etc/bind/zones/db.example.com";
dnssec-policy default;
inline-signing yes;
allow-transfer { key xfr-key; };
also-notify { 198.51.100.53; };
};
BIND creates the keys, signs the zone and handles rollovers. Get the DS record to give to your registrar with:
dig @203.0.113.53 example.com DNSKEY +noall +answer | dnssec-dsfromkey -f - example.com
Only add the DS record at the registrar once you've confirmed signed answers are served by every nameserver. If you remove signing later, remove the DS record first.
Keep It Updated
DNS software receives regular security fixes. Enable automatic security updates on the host, or for containers, pin a version and update it deliberately when new releases come out.
Monitoring
At minimum, monitor that each nameserver answers correctly from outside your network. A simple check you can run from cron or a monitoring tool:
#!/usr/bin/env bash
# Alert if any nameserver fails to answer or returns a different SOA serial
expected=$(dig @203.0.113.53 example.com SOA +short | awk '{print $3}')
for ns in 203.0.113.53 198.51.100.53; do
serial=$(dig @"$ns" example.com SOA +short +time=2 +tries=2 | awk '{print $3}')
if [ -z "$serial" ]; then
echo "CRITICAL: $ns did not answer"
elif [ "$serial" != "$expected" ]; then
echo "WARNING: $ns serial $serial, expected $expected"
fi
done
BIND exposes statistics through rndc stats and an optional statistics channel, and CoreDNS has a prometheus plugin that exports metrics for Prometheus and Grafana.
Putting Your Servers Live
When both servers answer correctly:
- Create glue records at your registrar for
ns1.example.comandns2.example.comwith their IP addresses. Registrars often call this "registering nameservers" or "child nameservers". - Change the domain's nameservers to
ns1.example.comandns2.example.com. - Verify the delegation once the change is live at the TLD:
dig example.com NS +trace | tail -n 8
- Watch your logs and monitoring for the first few days.
If you're moving from another provider, copy every record first, lower TTLs in advance, and handle any existing DNSSEC DS record before switching.
FAQ: Running Your Own DNS Server
You can for testing, but not for a production domain. Registries and good practice require at least two nameservers, and if a single server fails your domain stops resolving. Use two servers on separate networks, or pair one with a managed secondary.
DNS uses TCP for responses too large for UDP, such as DNSSEC-signed answers, and for zone transfers. Blocking TCP causes intermittent failures that are hard to diagnose.
They mean the same thing. BIND and the wider DNS community now use primary and secondary, and BIND accepts both forms in its configuration.
Yes. Secondaries compare the serial in the SOA record to decide whether to transfer the zone. If you forget to increase it, your changes won't reach the secondary.
For simple authoritative zones and containerised setups, yes. BIND still has the edge for advanced features such as RFC 2136 dynamic updates with fine-grained policies and long-established DNSSEC key management.
No. Keep authoritative and recursive roles separate. A public server that answers recursive queries for anyone can be abused in amplification attacks and is harder to secure.
It's a primary server that isn't listed in your NS records. It holds the master copy of your zones and transfers them to public secondaries, often run by a managed DNS provider, which answer all public queries.
Conclusion
Running your own DNS server with BIND or CoreDNS is very achievable: write a zone file, configure the server as authoritative with recursion off, validate the configuration, and test with dig before going live. BIND gives you the full traditional feature set, including zone transfers, dynamic updates and automated DNSSEC, while CoreDNS offers a small, container-friendly server with a simple Corefile.
The hard part isn't the setup but the responsibility. Run at least two servers on separate networks, secure transfers with TSIG, limit responses, keep the software updated and monitor every nameserver from outside. If that sounds like too much, a hidden primary feeding a managed secondary gives you control without carrying all the risk.


