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 the internal address 127.0.0.11 inside each container) that automatically resolves other containers' names (and any explicit network aliases) to their current IP address, as long as they're on the same user-defined network. This means application code can simply connect to http://api:3000 using the container's name as a hostname, and Docker's DNS resolves it to whatever IP that container currently has — even if the container is restarted and gets a new IP, the name-based lookup keeps working transparently.

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 attached to 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's networking implementation. Application code inside api can simply 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

Because api connects to db by name, not by hardcoded IP, this restart is transparent to api. The next DNS lookup for db simply returns whatever db's current IP is. The application code never needed to know or care that the underlying IP changed. This is precisely the same fundamental need Kubernetes Services and CoreDNS solve at cluster scale (see that stack's Service and DNS questions). The problem — ephemeral container IPs shouldn't be hardcoded into configuration — and the solution shape — a name-based DNS layer resolving to current IPs — are conceptually identical. They just operate at the scale of one Docker host versus an entire 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 be given one or more additional aliases beyond its own container name. This is useful when you want application configuration to reference a more generic, stable name (database) that isn't tied to the specific container's actual name, since that name 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

This is exactly why Docker Compose services can 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. So the DNS-based resolution described 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 completely different Docker network has no visibility into this DNS namespace at all. There's also no automatic load balancing built into this basic mechanism the way a Kubernetes Service provides. If multiple containers share the same name or alias, or you need actual load-balanced traffic distribution across multiple instances of a service, that's a capability Docker Compose, Swarm, or a real orchestrator like Kubernetes provides — not something the base DNS mechanism alone does for you. Hardcoding a container's IP instead of its name reintroduces exactly the fragility this mechanism exists to eliminate. It should be flagged in code review on sight.