Type something to search...
How to run your own DNS server with BIND or CoreDNS?

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:

RoleWhat it doesExample software
AuthoritativeAnswers for zones you own, such as example.comBIND, CoreDNS, Knot DNS, NSD, PowerDNS
RecursiveLooks up any name on behalf of clients and caches resultsUnbound, 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.53 for ns1 and 198.51.100.53 for ns2.
  • 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, producing ns1.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 @, so hostmaster.example.com. means hostmaster@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?

AspectBIND 9CoreDNS
MaturityDecades of production useMature, widely used in Kubernetes
Configurationnamed.conf with many optionsShort Corefile with plugins
Zone transfersFull AXFR/IXFR, NOTIFY, TSIGSupported via transfer and secondary plugins
DNSSECFull signing with dnssec-policy and automatic key rolloverOn-the-fly signing with the dnssec plugin
Dynamic updatesRFC 2136 with fine-grained update-policyNot supported by the file plugin
FootprintModerateSmall single binary
Best forTraditional authoritative DNS, hidden primariesContainers, 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:

  1. Create glue records at your registrar for ns1.example.com and ns2.example.com with their IP addresses. Registrars often call this "registering nameservers" or "child nameservers".
  2. Change the domain's nameservers to ns1.example.com and ns2.example.com.
  3. Verify the delegation once the change is live at the TLD:
dig example.com NS +trace | tail -n 8
  1. 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.

Tags :
Share :

Related Posts

What is the difference between an A record and a CNAME record?

What is the difference between an A record and a CNAME record?

The difference between an A record and a CNAME record is what they point to. An A record points a hostname directly at an IPv4 address, such

Dive Deeper
What are the best DNS providers for websites?

What are the best DNS providers for websites?

The best DNS providers for most websites are Cloudflare for a fast, free, easy starting point, Amazon Route 53 if you already run on AWS, **G

Dive Deeper
What are the best public DNS resolvers?

What are the best public DNS resolvers?

The best public DNS resolvers for most people are Cloudflare (1.1.1.1) for speed and privacy, Google Public DNS (8.8.8.8) for reliability and

Dive Deeper