CoursesDocker in depthDNS, discovery & publishing ports

DNS, discovery & publishing ports

How containers find and expose each other.

Intermediate12 min · lesson 12 of 30

Every container on a user-defined network comes with a tiny receptionist built in. Ask for a coworker by name and you get told which desk they are at today, even if they moved desks this morning. That receptionist is an embedded DNS server. DNS (Domain Name System) is the naming layer the whole internet runs on: names go in, addresses come back. Docker runs a small one of its own at the address 127.0.0.11, reachable from inside every container attached to that network. Your app asks for db. The resolver hands back db's current IP (Internet Protocol) address. Destroy db, recreate it, let it come back on a completely different address, and the name still lands on the right container. That is service discovery, and it is why you never write a container's address into your config.

terminal
$ docker network create appnet
c0ffee112233445566778899aabbccddeeff00112233445566778899aabbccdd
$ docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret postgres:16
9a3f7c1d2e4b6a8f0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b
$ docker run --rm --network appnet nicolaka/netshoot nslookup db
Server: 127.0.0.11
Address: 127.0.0.11#53
Non-authoritative answer:
Name: db
Address: 172.19.0.2

Names outlive IPs

Here is what that buys you. Halfway through a deploy the database container gets torn down and a fresh one takes its place. Docker's address pool hands the replacement whatever address happens to be free at that moment, so it can come back as 172.19.0.5 instead of .2. Your app never notices. It still asks for db, the resolver answers with the new address, and traffic keeps flowing. Run it yourself and watch the number change under you.

terminal
$ docker rm -f db
db
$ docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret postgres:16
7b21e9a4c6d8f0123456789abcdef0123456789abcdef0123456789abcdef012
$ docker run --rm --network appnet nicolaka/netshoot dig +short db
172.19.0.5

One container, several names

Sometimes one service has to answer to several names. Some old client is wired to reach postgres and nobody wants to redeploy it. Another team's config expects db.internal. A network alias is an extra name pinned onto a container, and the embedded resolver serves every alias exactly the way it serves the container name. You attach them when the container starts, as many as you need.

terminal
$ docker rm -f db
db
$ docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret \
--network-alias postgres --network-alias db.internal postgres:16
4c8e0f2a1b3d5e7f9012345678abcdef0123456789abcdef0123456789abcdef
$ docker run --rm --network appnet nicolaka/netshoot dig +short db postgres db.internal
172.19.0.6
172.19.0.6
172.19.0.6

A useful side effect falls out of this. Hand the same alias to two different containers and a lookup comes back with both addresses, the resolver rotating which one leads each time you ask. That is round-robin load balancing, taking turns in order, with nothing installed and nothing configured. Swarm (Docker Swarm, Docker's own clustering mode) builds its service virtual addresses on the same machinery, so the model you are forming right now carries straight over to services spread across a cluster.

What the resolver does with a name
a container looks up "db"
the query goes to the embedded resolver at 127.0.0.11
a container or alias on this network
embedded DNS answers
returns its current IP; follows restarts and aliases
a public domain name
forwarded upstream
the resolver passes it on to the host's DNS servers
containers on the default bridge
no name resolution
the lookup fails; move them to a user-defined network

Prove it: a real client, no address anywhere

A clean nslookup tells you the name resolved. It says nothing about whether anything reached the database. Those are two different events, and confusing them costs people whole afternoons. So close the loop. Start a throwaway psql client (the Postgres command-line tool) on the same network and point it at the hostname db, with no address typed anywhere in the command. If discovery is doing its job, the query runs and Postgres answers.

terminal
$ docker run --rm --network appnet -e PGPASSWORD=secret postgres:16 \
psql -h db -U postgres -c 'SELECT version();'
version
---------------------------------------------------------------------------
PostgreSQL 16.9 (Debian 16.9-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc ...
(1 row)

That is the promise made concrete. No address touched the command line. The client found Postgres by name alone, and the same command keeps working after db is destroyed and rebuilt on a different address. Point your application's connection string at db and you get identical behaviour in production, with no service registry to run and no extra plumbing to maintain.

Publishing: reaching in from outside

Everything so far happened inside the network. Containers on appnet find each other by name with zero ports published. Publishing does a different job. It punches a hole from the host into a container using NAT (Network Address Translation, the trick that rewrites addresses as packets cross a boundary), so something outside Docker can get in. Keep it for traffic that genuinely arrives from the outside world. The flag -p host:container maps a single port, and docker port prints what is currently live. Port mechanics get a lesson of their own next; here it is the outward-facing half of the same story.

terminal
$ docker run -d -p 8080:80 --name web --network appnet nginx:1.27
d41d8cd98f00b204e9800998ecf8427e1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
$ docker port web
80/tcp -> 0.0.0.0:8080
$ curl -sI localhost:8080 | head -1
HTTP/1.1 200 OK
The name updates. Your app might not.
Docker's resolver follows container restarts. The code running inside your container often does not. Plenty of runtimes cache a DNS answer and keep reusing it long after the address moved on. The JVM (Java Virtual Machine, the runtime Java programs execute on) is the famous offender. Its lookups are governed by the networkaddress.cache.ttl security property, and on older setups, or anything running with a security manager, that value defaults to caching forever. So you recreate db, its address changes, embedded DNS serves the new one, and your app keeps hammering an address nothing answers on until somebody restarts it. If you are counting on names surviving restarts, set a short DNS cache TTL (time to live, how long an answer stays valid) inside the app, or make it reconnect on failure rather than trusting one cached answer for the life of the process.

Names beat IPs inside user-defined networks

The old way of doing this was --link, which wrote entries into /etc/hosts and dumped environment variables into the container at start time. Both are snapshots. Neither one updates when the target container is recreated, which is precisely the moment you need them to. User-defined networks replaced all of that with a resolver at 127.0.0.11 that answers live, so a name written into config on Monday still finds the right container on Friday. Aliases cover the cases where one container honestly needs to answer to several names.

Keep the two directions apart in your head. The -p flag is for ingress, traffic arriving from outside the host. DNS names are for east-west traffic, the sideways kind between containers on the same network. A database almost never needs the first one. Publishing 5432 to 0.0.0.0 offers it to anything that can route to that host, and a defender reading docker port output should treat a published database port as a finding until somebody explains it. Attach the client to the same user-defined network instead and leave the port unpublished.

When a name refuses to resolve, check network membership before you touch application config. Two containers on different networks cannot find each other by name no matter how correct the connection string looks. Inspect both, compare the networks they are attached to, and you have usually found the problem inside a minute. In a Compose file (Docker Compose, the format that describes a multi-container app), name your networks and aliases explicitly rather than leaning on the defaults, because those defaults shift with the project name and the directory you happen to be sitting in.

Give the services other teams depend on stable aliases, and treat every connection pool as something that has to survive an address change. Most tickets filed as "DNS is broken" turn out to be a client holding on to an A record (the DNS record type that maps a name to an address) long past its expiry. Write down, per language, how long your web and database client libraries cache a lookup and what they do when a connection fails. That page is worth more at three in the morning than any diagram.

Treat a change to how a service is discovered the way you would treat a change to a firewall rule, and leave a trail. Record which aliases the container carried before and after, which host you ran the commands on, and the exact docker port line you saw when things were healthy. Recreating a container is cheap. Recreating it with flags nobody wrote down is not. If a teammate cannot repeat your steps from the ticket alone, the ticket is not finished yet.

Try this

Run these on a lab engine. Docker 24 or newer is fine. Read the sample output first so you know what a healthy result looks like before you lean on any of it in production.

terminal
$ docker network create appnet
$ docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret postgres:16
$ docker run --rm --network appnet alpine:3.20 nslookup db
Name: db
Address 1: 172.18.0.2 db.appnet
$ docker run --rm --network appnet alpine:3.20 wget -qO- db:5432 || true
# connection attempt proves name→IP; use psql healthchecks in real stacks
# STATUS: DNS READY for service discovery on appnet

Takeaway

Container-to-container traffic should travel over a user-defined network using DNS names. Publishing belongs to traffic arriving from outside the host and nothing else. When a name will not resolve, look at which networks the two containers are attached to before you start rewriting connection strings.

Quick check
01Two containers sit on the same user-defined network. From one of them, nslookup db comes back with the right address, but your app still cannot reach the database. What is the most likely cause?
Incorrect — If it were down, the lookup itself would have failed. A clean nslookup rules this out.
Correct — A resolved name gets you an address and nothing more. The process still has to accept connections on an interface other containers can reach, and 127.0.0.1 is reachable only from inside that one container.
Incorrect — Publishing only matters for reaching in from the host. Containers sharing a network talk to each other with no -p at all.
Incorrect — On a user-defined network it always sits at 127.0.0.11, and the successful lookup already proves the resolver is doing its job.
02You hand the same --network-alias web to two different containers. What does Docker's embedded DNS do when something looks up web?
Incorrect — No. Docker lets several containers share an alias, and that is exactly what makes the round-robin trick possible.
Incorrect — No. The resolver returns every container holding the alias, not only the earliest one.
Correct — A shared alias buys you round-robin with no install, the same machinery Swarm runs behind its service virtual addresses.
Incorrect — No. Upstream forwarding only happens for names the embedded resolver does not own, such as public domains.
03A Java app connects to db by name. You redeploy db, its address changes, embedded DNS starts serving the new one, and the app keeps hitting the old dead address until somebody restarts it. What is going on?
Correct — The resolver did serve the new address. The JVM's own cache pinned the old one until the process was restarted or told to expire it.
Incorrect — No. The embedded resolver does follow restarts. Here it served the new address and the client held the old one.
Incorrect — No. Containers on a shared network reach each other with no -p at all; publishing is only for traffic from outside the host.
Incorrect — No. The question says DNS is serving the new address, so the name resolves fine. The trouble is on the client side.

Related