How does container DNS-based service discovery work in a user-defined network?
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.