DNS Resolution Between Containers
How one container finds another by name instead of by a fragile, ever-changing IP address — and why Docker quietly runs a tiny DNS server inside every custom network to make that possible.
Every time you spin up a database container and a web application container and tell them to talk to each other, something has to answer one very basic question: “what address is ‘db’ actually sitting at right now?” IP addresses inside Docker are not fixed — a container can be destroyed and recreated with a completely new address, especially during a redeploy or a scaling event. If applications had to hardcode IPs, this would break constantly. Instead, Docker gives every custom network its own miniature phone book, updated automatically the instant a container joins or leaves, so containers can simply call each other by name and let Docker handle the lookup. This guide walks through exactly how that lookup works, what components make it possible, where it can go wrong, and how experienced teams design around it.
ACore Concepts
What DNS resolution is, generally
DNS resolution is the process of turning a human-friendly name, like db or api.internal, into a machine-friendly address, like 172.18.0.3. On the public internet this is handled by a global hierarchy of DNS servers. Inside a Docker environment, the same basic idea applies, but at a much smaller and more local scale — Docker maintains its own private, automatically updated directory just for the containers running on a given network.
Why containers cannot just use IP addresses directly
A container’s IP address is assigned when it starts and is not guaranteed to stay the same. If a container crashes and Docker restarts it, or if an orchestrator replaces it during a rolling update, it typically receives a new address. An application that had the old address hardcoded would simply stop working, with no obvious error pointing at the real cause. Name-based resolution sidesteps this entirely: the name stays constant even when the underlying address changes underneath it.
What makes container DNS different from ordinary DNS
Regular DNS servers are shared, relatively static infrastructure. Docker’s container DNS is per-network, ephemeral, and self-updating — it exists only for the lifetime of that particular Docker network, and its entries appear and disappear automatically as containers join and leave, with no administrator ever editing a DNS record by hand.
Think of a Docker network like a small office building with an automated directory board in the lobby. When a new employee (“container”) moves into an office, the board updates itself instantly to show their name and room number. Visitors never need to memorize room numbers — they just tell the front desk a name, and the board tells them exactly where to go, even if that person changed offices last week.
BArchitecture & Components
The shared namespace
A virtual network (commonly a user-defined bridge network) that groups containers together so they can reach each other. DNS resolution only works automatically among containers on the same custom network.
Docker’s built-in resolver
A lightweight DNS server that Docker runs automatically for every user-defined network, always reachable inside containers at the fixed address 127.0.0.11.
Each container’s private view
An isolated networking environment per container, which is what allows Docker to hand each container its own resolv.conf pointing at the embedded resolver.
The lookup instructions
A configuration file inside every container that tells its networking stack where to send name-lookup requests — Docker automatically points this at its embedded DNS server.
The living directory
An internal registry Docker maintains per network, mapping container names, container IDs, and network aliases to current IP addresses.
Extra nicknames
Additional names a container can register on a network besides its own name — commonly used so a service can be reached by a friendly, stable label regardless of its actual container name.
These pieces sit at different layers, but the relationship between them is simple: the network defines who is allowed to look each other up, the embedded DNS server is the thing doing the looking up, and resolv.conf is what quietly points every container at that server without any manual configuration.
CInternal Working
When a container starts on a user-defined network, Docker automatically writes an entry into that network’s internal service discovery table, associating the container’s name (and any aliases) with its assigned IP address. At the same time, Docker configures the new container’s resolv.conf so that any DNS lookup it performs is sent to 127.0.0.11 — the embedded DNS server — rather than to an external DNS provider.
When the application inside that container tries to connect to, say, db, the operating system’s standard name-resolution logic sends the query to 127.0.0.11 automatically, exactly as if it were a normal internet DNS lookup. The embedded server checks its internal table, finds the current IP address registered under the name db, and returns it. If no match is found on the local network table, the embedded server falls back to forwarding the query to whatever external DNS servers the Docker host itself is configured to use — which is how containers can still resolve ordinary internet domain names for outbound requests.
What happens on the default bridge network
This automatic name-based resolution only applies to user-defined networks. Containers attached to Docker’s original default bridge network do not get this embedded DNS behavior by default and generally must rely on legacy linking or explicit IP addresses instead — a detail that catches out people who assume every Docker network behaves identically.
The embedded DNS server is not a general-purpose DNS server you configure by hand — it is created, populated, and torn down automatically by Docker itself, in lockstep with containers joining and leaving that specific network.
DData Flow & Lifecycle
Picture a small application made of two containers on the same user-defined network: a web application container named web and a database container named db. The web application needs to open a connection to the database using the name db.
flowchart TD
A["Container 'web' starts, attaches to user-defined network"] --> B["Docker writes web to network's service discovery table"]
C["Container 'db' starts on same network"] --> D["Docker writes db plus its IP to service discovery table"]
B --> E["Docker configures web's resolv.conf to use 127.0.0.11"]
D --> E
E --> F["Application inside web requests connection to host 'db'"]
F --> G["Query sent to embedded DNS server at 127.0.0.11"]
G --> H["Embedded DNS checks network service discovery table"]
H --> I["Match found: db resolves to its current IP"]
I --> J["IP address returned to web container"]
J --> K["web opens TCP connection directly to db's IP"]
Fig. 1 — A name-based lookup from container to embedded DNS server to a live IP address, all happening automatically with no manual configuration.
Notice that the actual application-to-application traffic (the final TCP connection) never passes through the DNS server at all — DNS only answers the “where is it” question once, up front. If the db container is later removed and recreated with a new IP, Docker updates the service discovery table immediately, and the very next lookup from web automatically returns the new address, with no restart or reconfiguration required on the web side.
Compose and orchestrators lean on this heavily
Tools like Docker Compose create a user-defined network for every project by default specifically so that service names declared in the Compose file become usable hostnames automatically — which is why a typical Compose-based application can simply reference a database by its service name without any DNS setup written by the developer.
EAdvantages, Disadvantages & Trade-offs
Strengths
- Applications never need hardcoded IPs, which survive container restarts, redeploys, and scaling events cleanly
- Zero manual configuration — the embedded resolver and service discovery table are managed entirely by Docker
- Works out of the box with Docker Compose service names and Swarm service names, lowering the barrier for multi-container setups
Limitations
- Only works automatically on user-defined networks — the legacy default bridge network does not provide it
- Name resolution is scoped to a single network, so containers on different, unconnected networks cannot resolve each other without extra configuration
- An extra DNS lookup on every new connection adds a small amount of latency compared to a hardcoded IP, though this is negligible for almost all workloads
The trade-off in one sentence: automatic name resolution costs a small, usually invisible lookup delay and a requirement to deliberately put containers on the right network, in exchange for eliminating an entire category of “the IP changed and now nothing works” failures.
FSecurity
Every container on the same user-defined network can, by default, resolve and reach every other container on that network by name, regardless of whether they are actually meant to talk to each other. This means the network itself is the real security boundary, not the DNS system — a container should only ever be placed on the networks it genuinely needs, because being on a shared network implicitly grants both name resolution and network reachability. Placing an unrelated internal admin tool on the same network as a public-facing web application, purely out of convenience, means that web application could resolve and attempt to contact the admin tool directly.
Teams sometimes assume that because a service “isn’t listed anywhere public,” it is effectively hidden. Within a shared Docker network, any container can resolve any other container’s name through the embedded DNS server — there is no built-in access control on the lookup itself, only on whatever the destination service does after the connection arrives.
The practical mitigation is network segmentation: give logically separate parts of a system their own dedicated Docker networks, and only attach a container to a network when it has a real reason to reach the other members of that network. Combined with the destination service’s own authentication, this keeps the blast radius of “who can even find this container by name” appropriately small.
GDesign Patterns & Anti-patterns
Pattern
One dedicated user-defined network per logical application or bounded context, rather than one large shared network for everything.
Why it works
It keeps name resolution and reachability scoped to only the containers that genuinely belong together, making the DNS-based service directory itself act as a natural access boundary instead of an accidental open door.
Pattern
Using network aliases to give a service a stable, meaningful hostname that is independent of the actual container name.
Why it works
It decouples “what the container happens to be called right now” from “what name other services use to find it,” which is especially useful when swapping implementations behind the same logical service name during a migration.
Anti-pattern
Hardcoding a container’s IP address in application configuration “just to be safe,” instead of relying on its name.
Why it causes problems
It reintroduces exactly the fragility DNS resolution was built to remove — the very first time that container restarts with a new address, the hardcoded configuration silently breaks the connection.
Anti-pattern
Attaching every container in a large system to a single shared default-style network for convenience.
Why it causes problems
It maximizes what every container can resolve and reach, which is the opposite of least-privilege network design, and makes it much harder to reason about which services can actually talk to which others.
HBest Practices & Common Mistakes
Always use user-defined networks
Create explicit networks (or let Docker Compose create them) rather than relying on the legacy default bridge network, to get automatic DNS resolution at all.
Name containers and services meaningfully
Since the container or service name becomes the hostname other services use, clear naming pays off directly in readable connection configuration.
Connect only the networks that are needed
A container can join multiple networks; attach it only to the ones it must actually communicate on, keeping resolution scope intentional.
Assuming resolution works across unconnected networks
Two containers on entirely separate Docker networks cannot resolve each other by name unless something (like a shared network or a gateway) explicitly connects them.
Caching DNS results too aggressively in-app
Some application runtimes cache DNS lookups longer than Docker’s service discovery table changes, which can cause an app to keep talking to a stale IP after a container restart — check runtime-level DNS caching behavior for long-lived connections.
Confusing container name with network alias behavior
Relying on undocumented or accidental name collisions between containers instead of deliberately declared aliases makes a system fragile to rename.
IReal-World & Industry Examples
Docker Compose local development
A typical Compose file defining a web service and a database service lets the web service connect using the database’s service name directly, because Compose automatically creates a shared user-defined network for the whole project and registers each service by name.
Docker Swarm service discovery
In Swarm mode, services are resolved through a built-in virtual IP that load-balances across all healthy replicas of that service, so calling a service by name automatically spreads traffic across every running instance behind the scenes.
Microservice teams at scale
Engineering organizations running many small services, including companies like Netflix and Uber that popularized microservice architectures, depend on some form of automatic, name-based service discovery so that individual services can be redeployed constantly without every dependent service needing reconfiguration.
CI test environments
Continuous integration pipelines that spin up an entire application stack (web server, database, cache) for automated testing rely on the same embedded DNS behavior so test scripts can reference services by consistent names regardless of what IPs get assigned on that particular CI run.
JFrequently Asked Questions
KSummary and Key Takeaways
The essentials, in one place
- Names stay stable even when IPs change. Docker’s embedded DNS server keeps every container’s current address up to date automatically.
- 127.0.0.11 is the resolver. Every container on a user-defined network has its resolv.conf pointed at this fixed internal address.
- Only user-defined networks get this automatically. The legacy default bridge network does not provide name-based resolution out of the box.
- Resolution is scoped per network. Two containers can only resolve each other by name if they share a network.
- The network is the real security boundary, not the DNS system — anything on the same network can resolve and reach everything else on it by default.
- Network aliases decouple identity from naming, letting a stable hostname point at a container regardless of its actual name.
- This is the foundation Compose, Swarm, and most orchestrators build on to let services reference each other by simple, human-readable names instead of fragile IP addresses.