skip to content

In Docker, how does one container reach another container by name, and why does that same lookup fail when both containers run on the built-in default `bridge` network?

level: juniorimportance: must knowfreq 68%

answer

  1. nameserver 127.0.0.11
  2. user-defined net = DNS; default bridge = no DNS
  3. name, hostname, alias, compose service
  4. scope is per-network
  5. --link deprecated, stale /etc/hosts

basics

~20 s

On a user-defined network Docker runs an embedded DNS server at 127.0.0.11 that resolves container names and network aliases to container IPs. The built-in default bridge network has no such DNS: there you only have raw IPs or the deprecated --link flag.

solid answer

~50 s

Any container attached to a **user-defined** network gets `nameserver 127.0.0.11` in its `/etc/resolv.conf`. That is Docker's embedded DNS server, reachable only from inside that container's network namespace and answered by the Docker daemon. It resolves other containers on the *same* user-defined network by container name, by the container's hostname, by Compose service name, and by any `--network-alias`. Names it does not know (public domains) it forwards to the host's upstream resolvers. The legacy default `bridge` network - the one you get when you pass no `--network` - deliberately does not provide this. Containers there can only talk by IP, or via the deprecated `--link` flag, which wrote one-way `/etc/hosts` entries at start time and went stale as soon as the target was recreated. The practical rule: **always create a user-defined network** (`docker network create app`, or use Compose, which creates one per project) and address containers by name. Container IPs are pool-assigned and change on recreation; names do not.

code

bash · 7 lines
bash
docker network create app
docker run -d --name db --network app postgres:16
docker run --rm --network app alpine getent hosts db
# 172.18.0.2  db

docker run -d --name db2 postgres:16          # default bridge
docker run --rm alpine getent hosts db2       # no output, exit code 2

go deeper

for a junior

Know that user-defined networks (or Compose) give you container-name resolution and that the default bridge does not, and that you should never hardcode container IPs.

for a middle

Add the mechanism: 127.0.0.11 in /etc/resolv.conf, resolution scoped per network, aliases and Compose service names, and why --link is dead.

for a senior

Bring in operational consequences: names are stable across recreation but client-side DNS caching is not, and network scoping is a deliberate segmentation tool.

for a principal

Frame it as the discovery layer of a deployment: name-based addressing is what makes containers replaceable, and the same contract is what an orchestrator later provides at cluster scale.

## Why names instead of IPs Containers get IPs from Docker's IPAM pool (the default bridge uses roughly `172.17.0.0/16`; each user-defined network gets its own subnet). Those addresses are not stable: recreate a container and it may come back with a different one. Hardcoding IPs in configuration therefore breaks on every redeploy, so Docker provides name-based discovery. ## What a user-defined network gives you When you run `docker network create app` and attach containers with `--network app`, each container's `/etc/resolv.conf` contains a single entry: `nameserver 127.0.0.11`. That is the **embedded DNS server**. It is not a process inside your container image - the address is intercepted inside the container's network namespace and served by the Docker daemon, which knows every container attached to every network. The embedded resolver answers for: - the **container name** (`--name api`) - the container **hostname** (`--hostname`, defaults to the short container ID) - every **network alias** (`--network-alias`, or Compose `networks.<net>.aliases`) - in Compose, the **service name**, which is registered as an alias Crucially the scope is **per network**: a lookup only returns containers that share a network with the caller. Two containers on different user-defined networks cannot resolve each other, which is how you get isolation (a `frontend` network and a `backend` network, with the API attached to both). Anything the resolver does not recognise as a container name is **forwarded** to the DNS servers the daemon derived from the host (or from `--dns` / `daemon.json`), so `api.example.com` still resolves normally. ## Why the default bridge is different The default `bridge` network predates the embedded DNS and is kept for backwards compatibility. Containers on it get connectivity (outbound NAT, and each other's IPs are reachable) but **no DNS-based discovery**. `ping api` fails with a resolution error; `ping 172.17.0.3` works. The historical workaround was `--link`: ``` docker run --name db postgres docker run --link db:db myapp ``` `--link` injected an `/etc/hosts` line into the *linking* container at start time (and shared the target's environment variables). It is **deprecated**, for good reasons: it is one-directional, it is resolved once at start so a restarted `db` with a new IP leaves a stale entry, it forces a start order, and it does not work across hosts. Do not propose it in an interview except as history. ## What this looks like in practice Docker Compose does the right thing by default: it creates a project network and registers each service name, so `http://api:8080` works from any other service in the file with no configuration. If you are running raw `docker run` commands, one `docker network create` up front buys the same behaviour. Two details worth remembering. First, resolution returns the container's **internal** IP on that network, so you connect to the container's real port - not to any published host port. Second, `/etc/hosts` inside the container still only contains the container's own hostname mapping and localhost entries; peer lookups go through DNS, not through a hosts file that Docker keeps rewriting.

  • Why is `--link` deprecated, and what replaced it?
    `--link` injected a static `/etc/hosts` entry into the linking container when it started, plus the target's environment variables. It is one-way, requires a fixed start order, and goes stale the moment the target container is recreated with a new IP. User-defined networks with embedded DNS replaced it: resolution is dynamic, symmetric, and survives recreation.
  • A container is recreated and gets a new IP. Does the name keep working?
    Yes - the embedded DNS is backed by the daemon's live view of the network, so the next lookup returns the new address. The failure mode is on the client side: applications or runtimes that cache DNS results (for example a JVM with an infinite `networkaddress.cache.ttl`, or a long-lived connection pool) keep using the old IP until they re-resolve.
  • Can two containers on different user-defined networks resolve each other by name?
    No. Lookups are scoped to networks the caller is attached to. To let them talk, attach one container to both networks (`docker network connect`) or put them on a shared network. That scoping is the main tool for segmenting, for example, a public-facing tier from a database tier.

A user-defined network is like an office floor with its own internal phone directory: dial a colleague by name. The default bridge is the same floor with no directory - you must know the extension number, and numbers get reassigned.

saying these in an interview costs you the question

  • Claiming Docker keeps every container's name in every container's /etc/hosts - peer names come from DNS, not a hosts file
  • Recommending `--link` as the modern way to connect containers
  • Assuming name resolution works on the default bridge network
  • Thinking you must connect to the published host port to reach a container on the same network - you use its internal port
  • Believing containers on separate user-defined networks can resolve each other by name

context