Skip to content

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
TermMeaning
RegistrarWhere you buy the domain (Namecheap, Porkbun, Cloudflare)
RegistryOperates the TLD (Verisign for .com)
NameserverThe server that answers authoritatively for your domain
ZoneThe set of records for a domain
FQDNFully Qualified Domain Name — app.example.com. with the trailing dot
Apex / rootThe 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:

bash
dig NS example.com +short

Manage 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 ​

TypePoints toExample
AAn IPv4 addressapp.example.com → 203.0.113.10
AAAAAn IPv6 addressapp.example.com → 2a01:4f8:c17::1
CNAMEAnother name (alias)www.example.com → example.com
TXTArbitrary textSPF, DKIM, domain verification
MXMail server (with priority)example.com → 10 mx.provider.com
NSDelegates a zone to nameserversexample.com → ns1.cloudflare.com
CAAWhich CAs may issue certificates0 issue "letsencrypt.org"
SRVService location (port + host)Used by SIP, XMPP, Minecraft
PTRReverse: IP → nameSet 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    300

ALWAYS 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:

  • listen [::]:443 ssl; in Nginx (Level 14)
  • UFW showing (v6) rules (Level 5)
  • Verified: curl -6 https://app.example.com

CNAME ​

Type    Name   Value              TTL
CNAME   www    app.example.com    3600

CNAME RULES THAT CAUSE REAL OUTAGES

  1. A CNAME cannot coexist with any other record on the same name. If example.com has a CNAME, its MX records are invalid and your email stops working.
  2. You cannot CNAME the apex (example.com) in standard DNS. Providers offer ALIAS/ANAME/"CNAME flattening" (Cloudflare) which resolve it server-side. For a bare domain pointing at your own VPS, just use an A record.
  3. 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.com

Lower 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.

TTLDurationUse when
601 minuteActively migrating
3005 minutesGood default
36001 hourStable records
8640024 hoursRecords 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:

  1. Days before: lower the TTL to 300 on the records you will change.
  2. Wait for the old TTL to fully expire (if it was 86400, wait 24 hours). This is the step people skip.
  3. Change the record.
  4. Wait 5–10 minutes; verify from several resolvers.
  5. Keep the old server running for at least 24 hours — stragglers with badly-behaved caches will still arrive.
  6. 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"   3600

All 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) ​

bash
# LOCAL
dig -x 203.0.113.10 +short

Set 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 ​

bash
# 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

bash
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 cache

A number counting down on repeat queries confirms you are getting a cached answer.

nslookup and host ​

bash
nslookup app.example.com
nslookup app.example.com 1.1.1.1
host app.example.com
host -t MX example.com

Simpler output than dig, useful on Windows where dig may not be installed. dig is more precise — prefer it.

curl — does the whole path work? ​

bash
# 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 ​

bash
# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder

# Ubuntu (systemd-resolved)
sudo resolvectl flush-caches
resolvectl statistics

# Windows
ipconfig /flushdns

Browsers cache DNS independently: chrome://net-internals/#dns → "Clear host cache".

CHECK /etc/hosts BEFORE BLAMING DNS

bash
cat /etc/hosts

A 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 ​

ProblemCauseDiagnoseFix
NXDOMAINRecord does not existdig @<authoritative-ns> nameCreate the record; check for typos
Old IP still servedTTL cachingdig name — check the TTL countdownWait out the TTL; verify at the authoritative NS
Works for you, not othersYour local cache or /etc/hostsdig @1.1.1.1 nameFlush cache; remove hosts entry
Edits have no effectWrong provider is authoritativedig NS example.com +shortEdit at the provider the NS records point to
Email broke after a changeA CNAME added at the apexdig example.com ANYReplace the apex CNAME with an A record
Certbot fails validationDNS not propagated, or wrong IPdig app.example.com +shortWait; verify the A record matches your server
SERVFAILDNSSEC misconfiguration, or NS unreachabledig +trace nameCheck DNSSEC at the registrar; verify NS delegation
IPv6 users time outAAAA published, Nginx/UFW IPv4-onlycurl -6 -I https://app.example.comAdd listen [::]:443; enable UFW IPv6
www does not workRecord missingdig www.example.com +shortAdd a CNAME or A record and an Nginx server_name entry
Cloudflare proxy breaks Certbot HTTP-01Orange 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.net shows 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:

EffectDetail
DNS returns Cloudflare's IPsYour origin IP is hidden
TLS terminates at CloudflareYou still need a certificate on your origin
$remote_addr becomes CloudflareMust configure real_ip in Nginx (Level 14)
HTTP-01 challenges may failCloudflare intercepts port 80
WebSocketsSupported, 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:

  1. Create the server, note its IP.
  2. Create DNS records immediately with TTL 300. First-time records appear within minutes.
  3. Verify: dig app.example.com +short.
  4. 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 +short which 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
  • [ ] dig checked against the authoritative NS, a public resolver, and locally
  • [ ] /etc/hosts contains 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
  • [ ] --resolve used to test the new server before cutting over
  • [ ] Cloudflare (if used) set to Full (Strict), with real_ip configured in Nginx
  • [ ] Transactional email sent via a provider, not directly from the VPS

Next: Level 16 — SSL / HTTPS / Certbot →