How does container DNS-based service discovery work in a user-defined network?

6 minintermediatednsservice-discoverynetworking

Quick Answer

Docker runs an embedded DNS server, at address 127.0.0.11 inside each container, that resolves other containers' names (and any network aliases) to their current IP — as long as they're on the same user-defined network. So application code can just connect to http://api:3000 using the container's name as a hostname. Even if api restarts and gets a new IP, the name-based lookup keeps working.

Detailed Answer

The embedded DNS server

docker network create my-network
docker run -d --network my-network --name db postgres:16
docker run -d --network my-network --name api myapi:1.0
docker exec api cat /etc/resolv.conf
# nameserver 127.0.0.11

Every container on a user-defined network automatically has its DNS resolver pointed at 127.0.0.11 — Docker's own embedded DNS server, running as part of the Docker daemon. Application code inside api can just connect to db by name:

// Inside the "api" container
const client = new Client({ host: 'db', port: 5432 });   // resolves via Docker's embedded DNS

Why this survives container restarts, even with a changing IP

docker restart db
docker inspect db --format='{{.NetworkSettings.Networks.my_network.IPAddress}}'
# a possibly DIFFERENT IP than before the restart

api connects to db by name, not by hardcoded IP, so a restart is transparent to it. The next DNS lookup for db just returns whatever db's current IP is. The application code never needs to know the IP changed.

This is the same problem Kubernetes Services and CoreDNS solve at cluster scale: ephemeral container IPs shouldn't be hardcoded into configuration, so a name-based DNS layer resolves to the current IP instead. The idea is identical — Docker just applies it to one host instead of a whole cluster.

Network aliases — additional names for the same container

docker run -d --network my-network --network-alias database --name db postgres:16
const client = new Client({ host: 'database' });   // resolves to the "db" container, via its alias

A container can have one or more extra aliases beyond its own name. This is useful when you want your app config to use a generic, stable name like database, instead of a specific container name that might change between deployments or environments.

Compose does all of this automatically

services:
  db:
    image: postgres:16
  api:
    image: myapi:1.0
    environment:
      - DB_HOST=db     # "db" resolves automatically -- Compose creates a user-defined
                         #  network and names containers after their service name

Docker Compose services reference each other by service name with zero manual network setup. Compose automatically creates a user-defined bridge network for the whole project and names each container after its service. The DNS resolution above just works out of the box.

What this DNS mechanism doesn't do

Docker's embedded DNS only resolves names within the same user-defined network. A container on a different Docker network can't see this DNS namespace at all.

There's also no automatic load balancing built in. If multiple containers share a name or alias, or you need traffic balanced across several instances of a service, that's something Compose, Swarm, or Kubernetes provides — not the base DNS mechanism.

Hardcoding a container's IP instead of its name brings back exactly the fragility this mechanism exists to remove. Flag it in code review on sight.