DNS Zone File for BIND — A Complete Guide with a Free Generator
A DNS zone file is the text file that tells an authoritative nameserver everything about your domain: which IP serves the website, which host receives email, what nameservers should be queried, and how anti-spam machinery (SPF, DKIM, DMARC) is configured. Hand-writing one is the kind of task where a single missing trailing dot turns example.com. into example.com.example.com. and breaks half the records, and where an off-by-one serial number stops slave servers from updating for a week. This guide walks you through a working BIND zone file end to end — the SOA record, NS, A, MX, the email-authentication TXT records, and a CAA record for Let's Encrypt — plus the errors that will cost you an afternoon if you skip the validation step.
The reference we'll use is RFC 1035 (the original DNS specification), the BIND 9 documentation, and current best practice for SPF (RFC 7208) and DMARC (RFC 7489). Examples assume Ubuntu 22.04/24.04 with BIND 9.18 or newer, but the zone file syntax is identical across operating systems.
The DNS Zone generator — what it does and who it's for
The DNS Zone generator takes the parameters that change between domains — primary, secondary and tertiary nameserver IPs, the apex domain, the web server IP, and an optional mail server IP — and emits two files: a named.conf.local snippet that defines the zone for BIND, and the actual db.example.com zone file with SOA, NS, MX, A, CNAME, CAA, SPF and DMARC records pre-filled. You paste both into your BIND server, run named-checkzone, reload, and you're authoritative for the domain.
It's aimed at the admin who is moving a domain off a hosted DNS panel onto their own BIND infrastructure, the developer staging a new VPS who wants a sane default zone instead of copy-pasting from a five-year-old blog post, and anyone tired of forgetting to bump the SOA serial after every edit. The form is short on purpose — three IPs, a domain name, two optional fields — because everything else is either a fixed convention (refresh/retry/expire intervals, zone TTL) or a derivable default (the SOA email, the www CNAME, the SPF ip4: value).
What the generator saves is the small accumulating cost of typos: the missing dot at the end of ns1.example.com, the SPF record that ends in +all instead of -all, the DMARC record without the mandatory v=DMARC1 prefix. Every one of those is a half-hour debugging session if you don't catch it on the first read.
Hands-on — building a BIND zone file step by step
What we'll produce: a complete forward zone for example.com running on a primary nameserver at 213.76.166.194, with two slaves, a web server at 213.76.166.193, a mail server at 51.75.55.91, SPF and DMARC published, and a CAA record locking certificate issuance to Let's Encrypt. The end state is a zone that loads cleanly under named-checkzone and answers correctly to dig.
Step 1 — File location and BIND zone declaration
Forward zones on Debian/Ubuntu live in /etc/bind/zones/ by convention, with the filename db.<domain>. The zone itself has to be declared in /etc/bind/named.conf.local so BIND knows it's authoritative.
sudo mkdir -p /etc/bind/zones
sudo chown bind:bind /etc/bind/zones
// /etc/bind/named.conf.local
zone "example.com" {
type master;
file "/etc/bind/zones/db.example.com";
allow-transfer {
89.117.172.182;
51.75.55.91;
};
also-notify {
89.117.172.182;
51.75.55.91;
};
};
type master makes this server authoritative for the zone — slaves will pull from it. allow-transfer is a hard ACL: only listed IPs can request a full zone transfer (AXFR), so without it a stranger could dig AXFR example.com @your-ns and walk your entire DNS. also-notify makes the master push a NOTIFY message to the slaves when the zone changes, which combined with a higher serial triggers an immediate transfer instead of the slaves waiting out the refresh interval.
Step 2 — The zone file header: $TTL, SOA, and the email-as-dot trick
; /etc/bind/zones/db.example.com
;
; BIND data file for example.com
;
$TTL 3600
@ IN SOA ns1.example.com. root.example.com. (
2026050701 ; Serial
3600 ; Refresh
7200 ; Retry
2419200 ; Expire
7200 ; Negative Cache TTL
);
$TTL 3600 sets the default time-to-live — one hour — for any record that doesn't override it. Resolvers cache answers for that long, so a low TTL means changes propagate fast but query volume hitting your nameserver goes up. 3600 is a sensible default; drop it to 300 the day before you migrate.
@ is shorthand for the zone apex (example.com.). IN is the class — Internet, which is the only class anyone uses in 2026. SOA (Start of Authority) is the most important record in the zone: it identifies the primary nameserver and the responsible party.
The two fields after SOA look unusual. ns1.example.com. is the primary nameserver hostname — note the trailing dot, which makes it a fully qualified name; without the dot, BIND would append the origin and you'd end up with ns1.example.com.example.com. root.example.com. is the email address of the zone administrator, with the @ replaced by a dot. Read it as root@example.com. This convention exists because @ is already a special character in zone files.
The five numbers in parentheses are the SOA timers:
- Serial (
2026050701) — version number of the zone, inYYYYMMDDnnformat. Slaves compare this number against their own; if it's higher, they pull the zone. Bump it on every edit, or your changes won't propagate. - Refresh (
3600) — how often slaves check the master for a new serial. - Retry (
7200) — how long a slave waits before retrying after a failed refresh. - Expire (
2419200) — 28 days. If a slave can't reach the master for this long, it stops answering queries authoritatively. The RFC 1912 recommendation is 2-4 weeks. - Negative Cache TTL (
7200) — how long resolvers cacheNXDOMAINanswers. Was the "minimum TTL" in older docs; redefined by RFC 2308.
Step 3 — NS and MX records
; NS records
@ IN NS ns1.example.com.
@ IN NS ns2.example.com.
@ IN NS ns3.example.com.
; MX record
@ IN MX 10 mx.example.com.
The NS records list every nameserver authoritative for the zone. They must match what's set at the registrar (the "glue") — a mismatch produces an inconsistent delegation that some resolvers will still serve and others will refuse. You almost always want at least two NS records for redundancy; three is comfortable.
MX 10 mx.example.com. declares the mail exchanger. The 10 is the priority — when multiple MX records exist, mail servers try the lowest number first and fall back to higher numbers. A single MX is fine for most setups; introduce a backup only when you have somewhere for queued mail to land.
Step 4 — A and CNAME records for the website
; Host records
@ IN A 213.76.166.193
www IN CNAME @
@ IN A 213.76.166.193 points the apex (example.com) at the web server's IPv4 address. If you also serve IPv6, add a parallel AAAA record.
www IN CNAME @ makes www.example.com an alias for the apex. The @ on the right side resolves to example.com., so www returns the same A record as the apex. CNAMEs save you from updating two records when the web IP changes.
A common confusion: you can put a CNAME on www but not on the apex itself. RFC 1912 forbids a CNAME at a zone apex because the apex must hold SOA and NS records, and a CNAME can't coexist with other record types on the same name. If you need apex-level aliasing (looking at you, "naked domain pointing at a CDN"), you need a vendor-specific extension like ALIAS or ANAME, not a vanilla CNAME.
Step 5 — Nameserver glue: A records for ns1, ns2, ns3
; Nameserver A records
ns1 IN A 213.76.166.194
ns2 IN A 89.117.172.182
ns3 IN A 51.75.55.91
These are the in-zone glue records. If your nameservers live inside the domain they serve (a common setup — ns1.example.com is authoritative for example.com), you have a chicken-and-egg problem: to find the nameserver, the resolver has to query the nameserver. Glue records resolve it by being included in the parent zone's referral, so the resolver gets the IP without a recursive lookup. Make sure these IPs match the corresponding records at your registrar — that's where the parent (.com) glue lives.
Step 6 — SPF, DMARC and CAA: the modern email and TLS records
; MX records (additional records related to mail)
mx IN A 51.75.55.91
mx IN CAA 0 issue "letsencrypt.org"
; SPF record
@ IN TXT "v=spf1 ip4:51.75.55.91 -all"
; DMARC record
_dmarc IN TXT "v=DMARC1; p=reject; adkim=r; aspf=r;"
The mx A record gives the mail server its address — it's the host the MX record points to.
mx IN CAA 0 issue "letsencrypt.org" is a Certification Authority Authorization record, defined in RFC 8659. It says: only Let's Encrypt is allowed to issue certificates for this name. Public CAs are required to check CAA before issuing, so a stray DigiCert request would be refused. The 0 is the flags field; issue is the tag. Add issuewild if you want to control wildcard issuance separately.
The SPF record is a plain TXT entry at the apex. v=spf1 ip4:51.75.55.91 -all says: mail from example.com is only legitimate when sent from 51.75.55.91, and anything else should be hard-failed. The -all (hard fail) is stricter than ~all (soft fail) — start with ~all while you're still finding out who sends mail on your behalf. Crucially, there can only be one SPF record per domain. If you add a second TXT starting with v=spf1, receiving servers will randomly pick one or reject both, depending on the implementation.
The DMARC record lives at _dmarc.example.com and tells receiving servers what to do when SPF or DKIM fails alignment. p=reject is the strictest policy — bounce non-aligned mail. adkim=r and aspf=r are relaxed alignment modes. For a brand-new setup, Google's recommendation is to start at p=none, monitor reports for 48-72 hours via a rua= tag, and only then ratchet up to quarantine and reject.
DKIM isn't generated here because the public key comes from the mail server (Postfix/OpenDKIM, or your provider). You'll add it as a separate TXT record at selector._domainkey.example.com once the mail server gives you the value.
Step 7 — Validate and reload
sudo named-checkzone example.com /etc/bind/zones/db.example.com
sudo named-checkconf
sudo systemctl reload bind9
named-checkzone parses the zone file the same way BIND does and tells you about typos before they take down the nameserver. named-checkconf validates named.conf.local syntax. Reload (not restart) re-reads the zone without dropping in-flight queries. After reload, verify with:
dig +short SOA example.com @127.0.0.1
dig +short MX example.com @127.0.0.1
dig +short TXT example.com @127.0.0.1
The serial in the SOA answer should match what you put in the file. If it doesn't, BIND didn't pick up your edit — usually because you forgot to bump the serial.
Common mistakes and pitfalls
Forgetting the trailing dot on FQDNs
The number-one zone-file bug. ns1.example.com. (with dot) is a fully qualified name; ns1.example.com (no dot) gets the origin appended, becoming ns1.example.com.example.com.. named-checkzone flags this as not a valid hostname, but only sometimes — if the resulting name is syntactically legal, BIND will load the zone and your queries will silently return garbage.
Not bumping the SOA serial
Edits to the zone file are invisible until the serial increases. Slaves compare serials and only pull a new copy if the new one is higher. The standard convention YYYYMMDDnn (e.g. 2026050701 for the first edit on May 7th, 2026) keeps you ordered for life; pure incrementing integers also work but are easy to lose track of.
Two SPF records on the same domain
RFC 7208 states a domain MUST publish at most one SPF (v=spf1) record. Adding a second one is the most common reason mail starts disappearing after a marketing platform "helpfully" tells you to add their SPF. Merge instead: combine all sources into one record with multiple include: mechanisms.
; correct: one record, multiple sources
@ IN TXT "v=spf1 ip4:51.75.55.91 include:_spf.google.com -all"
CAA record blocking certbot
If you publish CAA 0 issue "letsencrypt.org" and then try to obtain a cert from a different CA (DigiCert, ZeroSSL, etc.), the issuance fails with an error like CAA record for example.com prevents issuance. The fix is either to add an issue line for that CA or to remove the restriction. The flip side: if you have CAA records that don't include Let's Encrypt and you're running the certbot client, Detail: CAA record for example.com prevents issuance is what you'll see in the certbot log.
CNAME at the zone apex
Putting @ IN CNAME ... is illegal per RFC 1912 because the apex already has SOA and NS records, and CNAMEs can't share a name with other record types. BIND throws CNAME and other data and refuses to load the zone. Solutions: use an A/AAAA record at the apex, or use a vendor-specific ALIAS/ANAME if you must point the apex at a hostname.
named[NNNN]: zone example.com/IN: loading from master file db.example.com failed: file not found
The path in named.conf.local doesn't match where the zone file actually lives, or BIND can't read it. Check the path, ownership (bind:bind on Debian/Ubuntu), and AppArmor: on Ubuntu, AppArmor's BIND profile only allows reads from a small set of directories. If you put zones somewhere unusual, edit /etc/apparmor.d/usr.sbin.named and reload AppArmor.
DMARC record without the mandatory v=DMARC1; p=... prefix
DMARC parsers stop at the first malformed tag, so a record like p=reject; v=DMARC1; rua=... is ignored entirely. The order matters: v=DMARC1 must be first, p= must be second. The Google Workspace docs are explicit about this and worth reading once.
Glue mismatch between registrar and zone
If your registrar has ns1.example.com → 1.2.3.4 and your zone file has ns1 IN A 5.6.7.8, resolvers will get inconsistent answers and DNSSEC validation (if you turn it on) will fail outright. Update both sides at the same time, and verify with dig +trace example.com from a clean resolver.
FAQ
What format should I use for the SOA serial number?
The almost-universal convention is YYYYMMDDnn, where nn starts at 01 and increments for each edit on the same day. So the first edit on May 7th, 2026 gives 2026050701. The format is purely a convention — BIND just needs an integer that goes up — but the date format keeps changes self-documenting and prevents accidental decreases when multiple admins edit a zone.
How do I make changes propagate faster than the TTL?
Lower the $TTL (and the per-record TTL on anything you'll be changing) before you make the change, wait at least the old TTL, then change the record. If $TTL was 3600 and you drop it to 300, the new TTL only takes effect for resolvers that fetch the record after the old one expires. Plan a TTL reduction at least 24 hours in advance of a migration.
Can I have a CNAME on the apex of my domain?
No, not with vanilla DNS. RFC 1912 forbids it because the apex has to hold the SOA and NS records, and a CNAME can't coexist with other record types on the same owner name. If you need to point example.com (not www.example.com) at a hostname like a CDN endpoint, you'll need ALIAS or ANAME — non-standard records implemented by some DNS providers (Cloudflare, Route 53) that resolve at query time.
How many MX records should I have?
One MX is enough for most setups — the mail server you actually run. Add a backup MX with a higher priority number only if you have somewhere reliable for the backup to queue and forward mail; a poorly configured backup MX is a known spam-attractor pattern because spammers target high-priority MX hosts hoping for looser filtering.
Do I need a separate DNS server, or can I use my registrar's nameservers?
For most domains the registrar's free DNS hosting is fine. Run your own BIND when you want full control over zone transfers, very low TTLs, split-horizon DNS (different answers for internal vs external clients), or DNSSEC signing on your terms. The Bind 9 generator on vps.pyrek.com.pl covers the full server-side configuration.
What is the Negative Cache TTL in the SOA record?
It's how long a resolver caches an NXDOMAIN answer ("this name doesn't exist") for any name in your zone. Set it lower if you're about to add new subdomains and want them to appear quickly; higher if your zone is stable and you want to reduce query load. Defined by RFC 2308 — older docs call it "minimum TTL", which is misleading.
How do I test that my zone file actually works?
Three commands, in order: named-checkzone example.com /etc/bind/zones/db.example.com parses the file, named-checkconf validates named.conf.local, and dig SOA example.com @127.0.0.1 (after reload) confirms BIND is serving it. For external visibility, query an external resolver: dig SOA example.com @1.1.1.1 once propagation has had time to spread.
Can I use the same zone file for IPv4 and IPv6?
Yes. Add AAAA records alongside the A records — same zone file, same names, just a different record type. @ IN AAAA 2001:db8::1 is the IPv6 equivalent of @ IN A 192.0.2.1. Resolvers that support IPv6 will prefer AAAA; others will fall back to A.
Should DMARC be set to p=reject from day one?
Don't. Start with p=none and a rua=mailto:dmarc-reports@example.com tag to collect aggregate reports for at least a week. The reports tell you which legitimate sources are sending mail under your domain that haven't been included in your SPF or signed with DKIM. Once those are clean, move to p=quarantine for a couple of weeks, then p=reject. Skipping the warm-up phase is the fastest way to silently drop legitimate mail.
Next steps
Generate your full zone file with your IPs and domain in the DNS Zone generator — fill in the form, paste the output into /etc/bind/zones/db.<your-domain>, and run named-checkzone.
Related topics that fit naturally with this one:
- Rsync directory sync generator — for backing up the work the new monitor enables.
If you prefer video, check out YouTube channel — practical Linux administration, Proxmox, and self-hosting tutorials.