Level 15 — DNS
DNS is how app.example.com becomes 203.0.113.10. It is also the layer where deployments most often appear to fail for reasons that have nothing to do with your server.
Domains and the hierarchy
app.example.com.
└┬┘ └──┬──┘ └┬┘└─ root (the trailing dot, usually implicit)
│ │ └──── TLD: .com
│ └────────── second-level domain: example
└──────────────── subdomain / hostname: app| Term | Meaning |
|---|---|
| Registrar | Where you buy the domain (Namecheap, Porkbun, Cloudflare) |
| Registry | Operates the TLD (Verisign for .com) |
| Nameserver | The server that answers authoritatively for your domain |
| Zone | The set of records for a domain |
| FQDN | Fully Qualified Domain Name — app.example.com. with the trailing dot |
| Apex / root | The bare domain, example.com, with no subdomain |
Nameservers
When you buy a domain, the registrar sets its nameservers. Those servers hold the actual records.
YOU MANAGE RECORDS WHEREVER THE NAMESERVERS POINT
If you set your domain's nameservers to Cloudflare's, then edits made in your registrar's DNS panel do nothing — the registrar is no longer authoritative. This causes hours of confusion.
Check who is actually authoritative:
dig NS example.com +shortManage records there.
WHICH DNS PROVIDER
Cloudflare (free) is the usual recommendation: fast global anycast, instant propagation of changes, free DDoS protection, an API for automation, and DNS-01 challenge support for wildcard certificates.
Your VPS provider's DNS (Hetzner DNS) is also fine and keeps everything in one account.
Your registrar's default DNS is usually the slowest and least featureful. Moving away costs nothing.
Record types
| Type | Points to | Example |
|---|---|---|
| A | An IPv4 address | app.example.com → 203.0.113.10 |
| AAAA | An IPv6 address | app.example.com → 2a01:4f8:c17::1 |
| CNAME | Another name (alias) | www.example.com → example.com |
| TXT | Arbitrary text | SPF, DKIM, domain verification |
| MX | Mail server (with priority) | example.com → 10 mx.provider.com |
| NS | Delegates a zone to nameservers | example.com → ns1.cloudflare.com |
| CAA | Which CAs may issue certificates | 0 issue "letsencrypt.org" |
| SRV | Service location (port + host) | Used by SIP, XMPP, Minecraft |
| PTR | Reverse: IP → name | Set at your VPS provider, not in your zone |
A and AAAA
Type Name Value TTL
A app 203.0.113.10 300
AAAA app 2a01:4f8:c17::1 300ALWAYS ADD AAAA IF YOU HAVE IPv6
Hetzner gives every server a /64 IPv6 block. Some mobile networks are IPv6-only with translation, and IPv6 avoids the translation hop.
But only if it actually works end to end. Publish an AAAA while Nginx listens on IPv4 only, and IPv6 users get timeouts while everything looks perfect to you. Requirements:
CNAME
Type Name Value TTL
CNAME www app.example.com 3600CNAME RULES THAT CAUSE REAL OUTAGES
- A CNAME cannot coexist with any other record on the same name. If
example.comhas a CNAME, its MX records are invalid and your email stops working. - You cannot CNAME the apex (
example.com) in standard DNS. Providers offerALIAS/ANAME/"CNAME flattening" (Cloudflare) which resolve it server-side. For a bare domain pointing at your own VPS, just use anArecord. - CNAME chains cost extra lookups. Each hop is another round trip.
TXT
Type Name Value
TXT @ "v=spf1 include:_spf.google.com ~all"
TXT _dmarc "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
TXT google._domainkey "v=DKIM1; k=rsa; p=MIGf..."
TXT _acme-challenge "<token from certbot DNS-01>"@ means the apex. TXT is used for domain ownership verification, email authentication (SPF/DKIM/DMARC), and Let's Encrypt DNS-01 challenges.
MX
Type Name Priority Value
MX @ 10 aspmx.l.google.com
MX @ 20 alt1.aspmx.l.google.comLower priority number = tried first.
MX RECORDS ARE INDEPENDENT OF YOUR WEB SERVER
Pointing example.com's A record at your VPS does not affect email — mail delivery uses MX. But if you replace an existing A record with a CNAME, you break MX (see above). When migrating a domain that already receives email, copy the MX and TXT records to the new provider before changing nameservers.
CAA
Type Name Value
CAA @ 0 issue "letsencrypt.org"
CAA @ 0 iodef "mailto:security@example.com"Restricts which Certificate Authorities may issue certificates for your domain. A cheap defence against mis-issuance.
A CAA RECORD CAN BLOCK CERTBOT
If you set 0 issue "digicert.com" and then run Certbot, Let's Encrypt checks CAA, sees it is not authorised, and refuses. The error is CAA record for example.com prevents issuance. Include letsencrypt.org.
TTL
Time To Live — how many seconds resolvers may cache a record.
| TTL | Duration | Use when |
|---|---|---|
| 60 | 1 minute | Actively migrating |
| 300 | 5 minutes | Good default |
| 3600 | 1 hour | Stable records |
| 86400 | 24 hours | Records that never change (MX, TXT) |
TTL IS THE MOST IMPORTANT THING TO PLAN BEFORE A MIGRATION
Changing an A record does not take effect immediately — resolvers worldwide serve the cached old value until its TTL expires. With TTL 86400, some users hit the old server for a full day.
The migration procedure:
- Days before: lower the TTL to 300 on the records you will change.
- Wait for the old TTL to fully expire (if it was 86400, wait 24 hours). This is the step people skip.
- Change the record.
- Wait 5–10 minutes; verify from several resolvers.
- Keep the old server running for at least 24 hours — stragglers with badly-behaved caches will still arrive.
- Raise the TTL back to 3600 once stable.
Skipping step 2 means the lowered TTL itself has not propagated, so the old high TTL is still in effect.
DNS for this deployment
Type Name Value TTL Purpose
A @ 203.0.113.10 300 example.com — landing/redirect
A app 203.0.113.10 300 frontend (Nuxt)
A api 203.0.113.10 300 backend (NestJS)
A www 203.0.113.10 300 redirects to apex
AAAA @ 2a01:4f8:c17::1 300
AAAA app 2a01:4f8:c17::1 300
AAAA api 2a01:4f8:c17::1 300
CAA @ 0 issue "letsencrypt.org" 3600All three names point at the same IP. Nginx distinguishes them by the Host header (Level 14). This is why one server can host many sites on one address.
USE A SUBDOMAIN FOR THE API
api.example.com rather than example.com/api gives you:
- Independent scaling — move the API to its own server later by changing one A record
- A separate certificate and separate Nginx logs
- Cleaner CORS configuration
- The ability to put a CDN in front of the frontend without proxying API traffic
The cost is a genuine cross-origin setup — you must configure CORS and, for cookie auth, SameSite=None; Secure or a shared parent domain (.example.com).
Reverse DNS (PTR)
# LOCAL
dig -x 203.0.113.10 +shortSet in your VPS provider's control panel, not your DNS zone (the reverse zone belongs to whoever owns the IP block).
Matters mainly if you send email directly from the server — receiving mail servers check that the sending IP's PTR matches its hostname, and reject or spam-file mismatches.
DO NOT SEND TRANSACTIONAL EMAIL DIRECTLY FROM YOUR VPS
Cloud IP ranges are widely blocklisted, you have no sending reputation, and deliverability will be poor regardless of how correct your SPF/DKIM/PTR are. Use Postmark, Resend, SendGrid, or Amazon SES. It is one API call and your password-reset emails actually arrive.
Verifying DNS
dig — the primary tool
# LOCAL
dig app.example.com # full response
dig app.example.com +short # just the answer
dig app.example.com A +short
dig app.example.com AAAA +short
dig example.com MX +short
dig example.com TXT +short
dig example.com NS +short
dig -x 203.0.113.10 +short # reverse lookup
# Query a SPECIFIC resolver — bypasses your local cache
dig @1.1.1.1 app.example.com +short
dig @8.8.8.8 app.example.com +short
dig @9.9.9.9 app.example.com +short
# Query the AUTHORITATIVE nameserver — the ground truth, no caching
dig @ns1.cloudflare.com app.example.com +short
# Full resolution path from the root
dig +trace app.example.com
# Show the remaining TTL of a cached record
dig app.example.com | grep -A1 "ANSWER SECTION"THE THREE-QUERY PROPAGATION CHECK
dig @ns1.yourdns.com app.example.com +short # 1. Authoritative — is the record correct at the source?
dig @1.1.1.1 app.example.com +short # 2. A public resolver — has it propagated?
dig app.example.com +short # 3. Your machine — what do you actually get?- 1 wrong → your record is wrong. Fix it in the DNS panel.
- 1 right, 2 wrong → still propagating. Wait.
- 1 and 2 right, 3 wrong → your local cache or
/etc/hosts. Flush it.
Reading dig output:
;; ANSWER SECTION:
app.example.com. 287 IN A 203.0.113.10
↑
remaining TTL in this cacheA number counting down on repeat queries confirms you are getting a cached answer.
nslookup and host
nslookup app.example.com
nslookup app.example.com 1.1.1.1
host app.example.com
host -t MX example.comSimpler output than dig, useful on Windows where dig may not be installed. dig is more precise — prefer it.
curl — does the whole path work?
# LOCAL
curl -I https://app.example.com
curl -sv https://app.example.com 2>&1 | head -30
# Test the server BEFORE DNS points at it
curl -I --resolve app.example.com:443:203.0.113.10 https://app.example.com
# Force IPv4 / IPv6
curl -4 -I https://app.example.com
curl -6 -I https://app.example.com--resolve LETS YOU TEST BEFORE CUTTING OVER DNS
--resolve host:port:ip makes curl connect to the given IP while still sending the correct Host header and doing TLS SNI for that name. You can fully verify Nginx routing, the certificate, and the application on a new server before changing any DNS record.
This turns a migration from "change DNS and hope" into "verify, then change DNS".
Flushing local caches
# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Ubuntu (systemd-resolved)
sudo resolvectl flush-caches
resolvectl statistics
# Windows
ipconfig /flushdnsBrowsers cache DNS independently: chrome://net-internals/#dns → "Clear host cache".
CHECK /etc/hosts BEFORE BLAMING DNS
cat /etc/hostsA leftover line from local testing (203.0.113.99 app.example.com) overrides DNS entirely, for you only. You then debug a "DNS problem" that does not exist. This costs people surprising amounts of time.
Common DNS problems
| Problem | Cause | Diagnose | Fix |
|---|---|---|---|
NXDOMAIN | Record does not exist | dig @<authoritative-ns> name | Create the record; check for typos |
| Old IP still served | TTL caching | dig name — check the TTL countdown | Wait out the TTL; verify at the authoritative NS |
| Works for you, not others | Your local cache or /etc/hosts | dig @1.1.1.1 name | Flush cache; remove hosts entry |
| Edits have no effect | Wrong provider is authoritative | dig NS example.com +short | Edit at the provider the NS records point to |
| Email broke after a change | A CNAME added at the apex | dig example.com ANY | Replace the apex CNAME with an A record |
| Certbot fails validation | DNS not propagated, or wrong IP | dig app.example.com +short | Wait; verify the A record matches your server |
SERVFAIL | DNSSEC misconfiguration, or NS unreachable | dig +trace name | Check DNSSEC at the registrar; verify NS delegation |
| IPv6 users time out | AAAA published, Nginx/UFW IPv4-only | curl -6 -I https://app.example.com | Add listen [::]:443; enable UFW IPv6 |
www does not work | Record missing | dig www.example.com +short | Add a CNAME or A record and an Nginx server_name entry |
| Cloudflare proxy breaks Certbot HTTP-01 | Orange cloud intercepts port 80 | — | Grey-cloud during issuance, or use DNS-01 |
DNS PROPAGATION IS NOT A GLOBAL BROADCAST
There is no push mechanism. Each resolver independently fetches the record when its cached copy expires. "Propagation" is just the slowest cache expiring.
Consequences:
- Checking
whatsmydns.netshows a mix of old and new — this is normal, not a fault. - You cannot speed it up. You can only have lowered the TTL beforehand.
- A record that never had a cached value appears instantly — first-time subdomains are immediate.
Cloudflare specifics
If you use Cloudflare with the orange cloud (proxy) enabled:
| Effect | Detail |
|---|---|
| DNS returns Cloudflare's IPs | Your origin IP is hidden |
| TLS terminates at Cloudflare | You still need a certificate on your origin |
$remote_addr becomes Cloudflare | Must configure real_ip in Nginx (Level 14) |
| HTTP-01 challenges may fail | Cloudflare intercepts port 80 |
| WebSockets | Supported, but check timeout settings |
SET CLOUDFLARE SSL MODE TO "FULL (STRICT)"
- Flexible — Cloudflare↔origin is plain HTTP. Your traffic is unencrypted across the internet, and it causes redirect loops with any HTTPS enforcement. Never use this.
- Full — encrypted, but Cloudflare does not validate your origin certificate (a self-signed cert passes).
- Full (Strict) — encrypted and validated. Use this, with your Let's Encrypt certificate on the origin.
LOCK YOUR ORIGIN TO CLOUDFLARE
With the proxy enabled, an attacker who discovers your origin IP can bypass Cloudflare entirely. Restrict inbound 80/443 to Cloudflare's IP ranges at your cloud firewall, and keep 22 open only to you.
Setting DNS before you have a server
The order that avoids waiting:
- Create the server, note its IP.
- Create DNS records immediately with TTL 300. First-time records appear within minutes.
- Verify:
dig app.example.com +short. - Then configure Nginx and run Certbot — which needs DNS to already resolve.
CERTBOT NEEDS DNS TO BE CORRECT FIRST
Let's Encrypt validates domain control by connecting to the IP your DNS points to. If DNS is wrong or not yet propagated, issuance fails — and repeated failures count against a rate limit of 5 failed validations per hostname per hour.
Always verify with dig before running Certbot. (Level 16)
Production Checklist — Level 15
- [ ] Domain registered; nameservers pointing at the DNS provider I actually manage
- [ ] Verified with
dig NS example.com +shortwhich provider is authoritative - [ ] A records for
@,app,api,www→ server IP - [ ] AAAA records if IPv6 is fully working end to end
- [ ] IPv6 verified with
curl -6 -I https://app.example.com - [ ] TTL 300 during setup and migrations
- [ ] CAA record allowing
letsencrypt.org - [ ] MX and TXT records preserved if the domain handles email
- [ ] No CNAME at the apex
- [ ]
digchecked against the authoritative NS, a public resolver, and locally - [ ]
/etc/hostscontains no stale test entries - [ ] DNS verified before running Certbot
- [ ] Migration plan: lower TTL, wait out the old TTL, cut over, keep the old server 24h
- [ ]
--resolveused to test the new server before cutting over - [ ] Cloudflare (if used) set to Full (Strict), with
real_ipconfigured in Nginx - [ ] Transactional email sent via a provider, not directly from the VPS