Before a machine can send a single packet to example.com or db-01, it has to turn the name into an IP address. That lookup is DNS, the Domain Name System. When it fails, nothing else gets a chance to run, which is why "it's always DNS" is a common saying among people who look after servers.
This topic covers where a lookup goes and what a domain publishes. By the end you can read /etc/hosts, /etc/nsswitch.conf and /etc/resolv.conf, follow a lookup with dig, ask for any record type, and know which type to use for which job.
Names are a tree
DNS names are read from right to left. www.example.com. has a hidden dot at the end, which stands for the root of the tree. Under the root are the top-level domains (TLDs) such as com, org and net. Under com is example, and under that, www.
Each level is run by different people on different servers, and each level only knows who is in charge of the level below it:
| Level | Example | Knows |
|---|---|---|
| Root servers | a.root-servers.net | Which servers run each TLD |
| TLD servers | a.gtld-servers.net for com | Which servers run each domain under it |
| Authoritative servers | The name servers for example.com | The actual records for example.com |
An authoritative server is the one that holds the real answer for a domain, because its owner put the records there. Every other server only passes answers along or remembers them.
Stub resolvers and recursive resolvers
Walking down that tree takes several queries, and most machines do not do it themselves. The work is split between two parts:
- The stub resolver is the small piece of code in every program's system library. It asks one question, "what is the address of example.com?", to one server, and waits for a full answer.
- A recursive resolver takes that question and does the walk: root, then TLD, then authoritative. It returns the final answer to the stub and keeps a copy.
Your home router, your internet provider, your company network and cloud networks all provide a recursive resolver. In a cloud virtual network, it usually sits at a fixed address inside the network and is set up for you.
In the lab, the stub resolver on each container asks Docker's built-in DNS service, which answers for container names itself and passes everything else on to the resolver your computer uses.
Before DNS: /etc/hosts and nsswitch.conf
A Linux machine does not always ask DNS. /etc/nsswitch.conf lists the sources it tries, in order, for each kind of lookup. For host names:
root@alpha:/# grep hosts /etc/nsswitch.conf
hosts: files dns
files means /etc/hosts, and dns means asking a DNS server. The first source that has an answer wins. Look at alpha's hosts file:
root@alpha:/# cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00:: ip6-localnet
ff00:: ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
10.10.0.2 alpha
Each line is an address followed by one or more names. Docker wrote these lines; you can add your own, and they take effect at once. To see what the system resolves, as a program would, use getent. It follows the order in nsswitch.conf, so it sees /etc/hosts:
root@alpha:/# getent hosts example.com
104.20.23.154 example.com
172.66.147.243 example.com
root@alpha:/# echo "192.0.2.50 example.com" >> /etc/hosts
root@alpha:/# getent hosts example.com
192.0.2.50 example.com
Because files comes first, /etc/hosts overrides real DNS. ping, curl and most programs now go to 192.0.2.50, an address that goes nowhere. This is useful for testing a new server before changing DNS, and a classic cause of "it works on every machine except this one". Remove the line when you are done. In a container, /etc/hosts is managed by Docker and sed -i cannot replace it, so filter it and write it back:
root@alpha:/# grep -v 192.0.2.50 /etc/hosts > /tmp/hosts && cat /tmp/hosts > /etc/hosts
/etc/resolv.conf
When the lookup reaches the dns step, the stub resolver reads /etc/resolv.conf to find which server to ask:
root@alpha:/# cat /etc/resolv.conf
# Generated by Docker Engine.
# This file can be edited; Docker Engine will not make further changes once it
# has been modified.
nameserver 127.0.0.11
options ndots:0
...
The lines that matter:
nameserveris the address of the recursive resolver. You can list up to three; the next is tried if the first does not answer. Here it is127.0.0.11, Docker's DNS service, reached on a loopback address inside every container.search, when present, lists domains to try appended to short names, sodb-01can meandb-01.lab.internal.optionsadjusts the behaviour, such as how many dots a name needs before it is tried as a full name first (ndots).
On an Ubuntu server outside a container, /etc/resolv.conf often says nameserver 127.0.0.53. That is systemd-resolved, a local caching service that forwards to the real resolvers. resolvectl status shows which ones.
Following a lookup with dig
dig sends a DNS query and prints the full answer. Unlike getent, it ignores /etc/hosts and asks the server in resolv.conf directly, so it tests DNS alone:
root@alpha:/# dig example.com
; <<>> DiG 9.18.39-0ubuntu0.24.04.8-Ubuntu <<>> example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44683
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;example.com. IN A
;; ANSWER SECTION:
example.com. 377 IN A 104.20.23.154
example.com. 377 IN A 172.66.147.243
;; Query time: 4 msec
;; SERVER: 127.0.0.11#53(127.0.0.11) (UDP)
;; WHEN: Sun Oct 25 07:27:43 UTC 2026
;; MSG SIZE rcvd: 61
Your addresses and numbers may differ. Read it from the top:
status: NOERRORmeans the name exists.NXDOMAINwould mean it does not;SERVFAILthat the resolver could not get an answer.- The flags
rd ramean "recursion desired" and "recursion available": dig asked for the full walk, and the server does it. - The question is the name and record type asked for,
Afor an IPv4 address. - The answer has one line per record. The next section reads them.
SERVERshows which resolver answered.
Two shorter forms are more useful day to day. +short prints only the values, and +noall +answer prints just the answer lines:
root@alpha:/# dig +short example.com
104.20.23.154
172.66.147.243
To see the walk the resolver does, run dig +trace +nodnssec example.com @a.root-servers.net. dig then starts at a root server and follows each referral itself, printing one group per step: a root server names the com servers, a com server names the servers for example.com, and one of those, the authoritative server, gives the answer. The @ is needed because Docker's DNS service does not answer the first question +trace asks, and +nodnssec hides the signature records that would otherwise fill the screen.
Records
A DNS name holds a set of records, each with a type: one type for its IPv4 address, another for its mail servers, another for free text that other services check. Every record has five parts, which dig prints in this order:
example.com. 377 IN A 172.66.147.243
name TTL class type value
The TTL is how many seconds a resolver may cache the record. The class is always IN (internet) in practice. The format of the value depends on the type. dig asks for A records unless you name another type after the domain, as in dig +short example.com MX.
| Type | Maps a name to | Typical use |
|---|---|---|
| A | An IPv4 address | Websites, servers |
| AAAA | An IPv6 address | The same, over IPv6 |
| CNAME | Another name | Aliases, pointing at a cloud service |
| MX | Mail servers, with priorities | Receiving email |
| TXT | Free text | SPF, domain verification |
| NS | Authoritative name servers | Delegating a domain |
| SOA | Zone details | Serial, timers, negative TTL |
| PTR | A name, from an address | Reverse lookups, with dig -x |
A name can have several records of the same type. example.com has two A records, and a client may use either, which is a simple way to spread traffic over more than one server. A name can also have both A and AAAA records, and a client that has IPv6 usually tries it first.
A CNAME says "this name is another name for that one". Its value is a name, and the resolver carries on and looks that one up:
www.example.com. 300 IN CNAME web-01.example.com.
web-01.example.com. 300 IN A 203.0.113.10
With that, www always follows web-01: when the server moves, you change one A record. CNAMEs are common for pointing your own name at a cloud service, such as a load balancer, whose addresses the provider changes when it needs to. A name with a CNAME cannot have any other records, so the bare domain, example.com itself, cannot be a CNAME. Some DNS providers offer an "alias" record type to work around that.
MX records carry a priority, and lower numbers are tried first. example.com publishes MX 0 ., a null MX, which states that the domain accepts no email at all. TXT records are mostly used to prove things to other services. v=spf1 -all is an SPF record, listing which servers may send email for the domain (here, none). A random-looking string is usually a verification token: a service asks you to publish a value, then checks for it, which proves you control the domain.
NS records are the links in the tree that the trace followed. A domain should have at least two, so one can fail. When you move a domain to a new DNS provider, changing the NS records at the registrar, where you bought the domain, is the step that makes the switch.
The SOA record holds facts about the zone itself:
root@alpha:/# dig example.com SOA +short
elliott.ns.cloudflare.com. dns.cloudflare.com. 2418004112 10000 2400 604800 1800
In order: the primary name server, the administrator's mail address with the @ written as a dot, a serial number that goes up with each change, three timers for secondary servers, and last the negative TTL, here 1800 seconds. When a name does not exist, resolvers cache the NXDOMAIN answer for that long.
When results look odd, ask one of the domain's own name servers with @, which shows what the domain really publishes with no cache in between. dig +short example.com AAAA @$(dig +short example.com NS | head -1) runs the inner dig first to find a name server. In the lab this matters for AAAA records: Docker Desktop's DNS service does not pass IPv6 answers to containers without IPv6, so the lab's resolver returns nothing for them.
Caching and TTL
Walking the tree for every lookup would be slow and would overload the root servers. So every resolver keeps answers in a cache, for as long as the record's TTL says. Run dig example.net | grep "Query time" twice: the first lookup goes out to the internet and takes hundreds of milliseconds, and the second comes from a cache in a few. The TTL in an answer from a resolver is normally the time left on its cached copy, counting down, though Docker Desktop's resolver does not always show that.
Caching is why DNS changes are not instant. If you move web-01.example.com to a new address, resolvers that cached the old one keep using it until the TTL runs out. The usual practice is to lower the TTL a day before a planned change, make the change, and raise it again afterwards. The negative TTL works the same way for names that do not exist yet: look up a name before you create it, and you may wait for that long before the new record appears.
Tasks
Task 1: Find where a name comes from
On alpha, add 203.0.113.10 db-01 to /etc/hosts. Then look up db-01 with getent hosts, ping -c 1 -W 1 and dig +short. Explain why one of the three disagrees with the others. Remove the line afterwards.
Check your work
root@alpha:/# echo "203.0.113.10 db-01" >> /etc/hosts
root@alpha:/# getent hosts db-01
203.0.113.10 db-01
root@alpha:/# dig +short db-01
root@alpha:/#
getent and ping both use the system's resolver, which checks /etc/hosts first, so they find 203.0.113.10; ping gets no replies, because nothing has that address. dig asks the DNS server directly and skips /etc/hosts, and DNS has no db-01, so it prints nothing. The common mistake is testing with dig alone and concluding a name does not resolve, when programs on the machine resolve it fine through /etc/hosts.
Task 2: Profile a domain
Write down every record of the types A, AAAA, MX, TXT and NS that example.org publishes. Use +short, and ask the authoritative server for AAAA. From the results, answer: does example.org accept email, and who hosts its DNS?
Check your work
root@alpha:/# for t in A MX TXT NS; do echo "== $t"; dig +short example.org $t; done
== A
172.66.157.237
104.20.26.136
== MX
0 .
== TXT
"_uyh0vgv5ukyofwj3ddwspnni01vmlyy"
"v=spf1 -all"
== NS
katelyn.ns.cloudflare.com.
mitch.ns.cloudflare.com.
root@alpha:/# dig +short example.org AAAA @$(dig +short example.org NS | head -1)
2606:4700:10::6814:1a88
2606:4700:10::ac42:9ded
The null MX, 0 ., says it accepts no email, and the SPF record says no server may send mail for it either. The NS records name the DNS provider. Values change over time, so yours may differ. The likely mistake is asking for AAAA through the lab resolver, getting nothing, and concluding the domain has no IPv6.
Quiz
1. What is the difference between a stub resolver and a recursive resolver?
Show answer
The stub resolver is part of every machine and asks one question of one server, expecting a complete answer. The recursive resolver does the work of walking the tree from the root to the authoritative server, and caches what it finds.
2. /etc/nsswitch.conf says hosts: files dns. A name is in both /etc/hosts and DNS with different addresses. Which address does curl use, and which does dig show?
Show answer
curl uses the address in /etc/hosts, because files comes first. dig shows the DNS address, because it ignores /etc/hosts and asks the DNS server directly.
3. What is the difference between an A record and a CNAME record?
Show answer
An A record maps a name directly to an IPv4 address. A CNAME maps a name to another name, which the resolver then looks up in turn. A name with a CNAME cannot have any other records.
4. A service on a server cannot be reached under its new name: you added an A record for api.example.com ten minutes ago, the authoritative server returns it, but your resolver still says NXDOMAIN. You had tried the name once before creating it. What is going on?
Show answer
The resolver cached the earlier NXDOMAIN answer for the negative TTL, the last value in the domain's SOA record. Until that expires, it keeps saying the name does not exist. You can wait, query the authoritative server with dig @ to confirm the record is right, or flush the resolver's cache if you control it.