Key Takeaways
- Two containers on the same Docker host does NOT mean they can see each other via DNS. They must be attached to the same user-defined bridge network — the default bridge network has zero service discovery.
- Container name resolution works only within a user-defined network.
--linkis legacy garbage that writes static entries into/etc/hostsand breaks on container restart. network_mode: hostsilently kills Docker’s built-in DNS. Your container becomes invisible to the resolver. This is the sneakiest pitfall we hit in production.- Port conflicts are not DNS failures. Don’t waste hours chasing DNS when the real issue is
address already in use. - Production fix: use docker-compose with explicit networks + health checks +
depends_onconditions. Never rely on startup order luck.
1. The Problem: Same Host, Two Containers, No DNS
Let me tell you about the mess we walked into last week. We’re running two containers on a single Ubuntu 22.04 server — an Nginx reverse proxy and a Node.js backend API. The Nginx config points at proxy_pass http://api:3000, which should hit the API container by name, right?
Wrong.
Inside the Nginx container, curl http://api:3000 returns Could not resolve host: api.
My first reaction was: “Isn’t this like… basic Docker functionality?”
Nope. Docker’s DNS resolution only kicks in under specific conditions — and that condition is exactly what most people miss: both containers must join the same user-defined bridge network. The default bridge network (the one containers land on when you don’t specify --network) does zero service discovery.
This problem has massive search volume. Reddit and Hacker News have fresh threads about it every single week. And the answers are all over the map — some clueless people still recommend --link (deprecated garbage), others say edit /etc/hosts (a band-aid that doesn’t survive restarts), and a few just throw up their hands and hardcode IPs.
2. Architectural Deep Dive: How Docker’s Built-in DNS Actually Works
2.1 The Default Bridge Network Trap
When you run docker run without --network, your container lands on the default bridge network. That network has three fatal flaws:
- No DNS-based service discovery. Containers can only talk via IP addresses — container names don’t resolve.
- Container IPs are not stable. Restart a container and its IP can change. Hardcoding IPs is planting a landmine.
- No network-level isolation policies. Everything can talk to everything at L2, but that’s not DNS-level control.
So the mental model “same Docker host = containers can talk to each other” is fundamentally wrong. This wrong assumption has burned more engineers than I can count.
2.2 The Magic of User-Defined Networks
When you run docker network create mynet and attach both containers to mynet, Docker spins up an embedded DNS resolver (listening at 127.0.0.11:53). That resolver automatically maps container names to IPs.
The mechanism is elegant — the Docker daemon maintains a name-to-IP mapping table. Containers register themselves on network attach, and unregister on destroy. This is worlds better than manually editing /etc/hosts.
graph TD
A[Nginx container<br/>container_name: nginx-proxy] -->|DNS query for 'api'| B[Docker embedded DNS<br/>127.0.0.11:53]
B -->|returns 172.20.0.3| A
A -->|HTTP request :3000| C[API container<br/>container_name: api<br/>IP: 172.20.0.3]
D[User-defined network 'mynet'<br/>bridge driver] --- A
D --- C
E[Docker daemon<br/>maintains name→IP map] --> B
2.3 --link Is a Relic of the Past
Old tutorials still push the --link flag, which is from Docker’s prehistory. --link writes a static entry into the container’s /etc/hosts — the moment the target container restarts and gets a new IP, that entry is dead. And --link only works for a single container, not network-level service discovery.
Docker officially deprecated --link years ago, but poorly-maintained blog posts keep the myth alive.
3. Step-by-Step Troubleshooting: Symptom to Root Cause to Fix
3.1 Step 1: Confirm the Symptom
From inside the target container, test DNS resolution:
# Enter the Nginx container
docker exec -it nginx-proxy bash
# Test DNS resolution
getent hosts api
# Empty output = DNS resolution failed
# IP output = resolution works, problem is elsewhere
# Test raw network connectivity (bypass DNS)
ping -c 3 172.20.0.3
If getent hosts api comes back empty, the DNS layer is dead. If it returns an IP but curl still fails, move on to port and firewall checks.
3.2 Step 2: Check Network Configuration
# Check which networks each container is attached to
docker inspect nginx-proxy --format '{{json .NetworkSettings.Networks}}'
docker inspect api --format '{{json .NetworkSettings.Networks}}'
# List all networks
docker network ls
The decisive check: if both containers aren’t on the same user-defined network, DNS resolution will fail. That’s the answer for 90% of the cases.
3.3 Step 3: Confirm the Container Name
Docker DNS resolves container names, not hostnames, not service names. This trips people up constantly — in docker-compose you write a service_name, but the actual container name becomes projectname_servicename_1 (e.g., myapp_api_1).
# See actual container names
docker ps --format "table {{.Names}}\t{{.Ports}}"
If your code references http://api:3000 but the container is actually named myapp_api_1, DNS resolution will fail. Period.
3.4 Step 4: Fix It — Create a Custom Network and Attach
# Create a user-defined bridge network
docker network create app-network
# Start containers with the network specified
docker run -d --name api --network app-network my-api-image
docker run -d --name nginx-proxy --network app-network -p 80:80 my-nginx-image
# Now test DNS resolution
docker exec nginx-proxy getent hosts api
# Should output something like 172.18.0.2
3.5 Step 5: Verify the Full Chain
# Test the HTTP request from inside the Nginx container
docker exec nginx-proxy curl -v http://api:3000/health
# 200 = fixed
# Timeout or connection refused = check if the API binds to 0.0.0.0, not 127.0.0.1
4. The Sneaky Production Pitfall: network_mode: host
While investigating this exact issue, our team hit a far more insidious variant — the API container used network_mode: host.
That mode makes the container share the host’s network stack directly, completely bypassing Docker’s network layer. The consequences:
- The container doesn’t participate in any Docker network — the embedded DNS resolver can’t see it.
- Other containers trying to reach it by name will always fail.
- Your only access path is via
localhostor the host machine’s IP.
# This is the WRONG way
services:
api:
network_mode: host # Makes the container invisible to Docker DNS
nginx:
ports:
- "80:80"
The right approach: drop network_mode: host, put every container on the same user-defined network, and expose services through port mappings.
5. Side-by-Side Comparison: All the Options
| Approach | DNS Service Discovery | IP Stability | Network Isolation | Maintenance Cost | Best For |
|---|---|---|---|---|---|
| Default bridge | ❌ No | ❌ Unstable | ❌ None | Low | Single-container testing |
--link | ⚠️ Static mapping | ❌ Unstable | ❌ None | High (manual updates) | Deprecated, avoid |
| User-defined bridge | ✅ Automatic | ✅ Stable | ✅ Subnet support | Low | Production default |
| host network | ❌ No | N/A | ❌ None | Low | Extreme performance needs |
| docker-compose default network | ✅ Automatic | ✅ Stable | ✅ Subnet support | Minimal | Multi-container microservices |
From a maintenance-cost standpoint, docker-compose’s default network mechanism is the least painful — define services in one file, and Docker automatically creates the network and handles DNS for you.
6. The War Story: How We Burned 3 Hours on This
Back to our team’s saga. We were debugging a flaky production issue — the Nginx container was throwing intermittent 502 Bad Gateway errors, while monitoring showed the API container was alive and healthy.
We restarted the API container several times. Didn’t help. Then we hopped into the Nginx container and manually ran curl api:3000 — and noticed DNS resolution was working sometimes. That’s when we realized this was a DNS problem, not an app crash.
The root cause turned out to be — the API container in docker-compose didn’t explicitly specify a network, and the Nginx container lived in a different compose file. Both fell onto separate default networks. Same physical host, but the network layer was completely disconnected.
The fix was simple: unify both services into a single docker-compose file, or explicitly attach both to the same external network.
# docker-compose.yml
version: "3.8"
networks:
app-net:
driver: bridge
services:
api:
image: my-api:latest
networks:
- app-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
networks:
- app-net
depends_on:
api:
condition: service_healthy
This depends_on + healthcheck combo is the key insight. Using depends_on alone only guarantees boot order, not readiness — which causes random 502s when the API is slow to start. Adding the health check makes Nginx wait until the API is genuinely accepting connections.
7. Performance & Security: What the Senior Engineer Knows
Does Docker’s embedded DNS become a bottleneck? We measured it. Single DNS lookups (with local cache hits) take microseconds to low milliseconds. For 99.9% of applications, the overhead is noise. If you’re pushing thousands of new connections per second without connection reuse, DNS isn’t your bottleneck — your connection handling is.
From a security angle, user-defined networks give you real isolation benefits over the default bridge. You can carve out subnets and control which containers talk to which. For multi-tenant setups, use a separate network and subnet per application to prevent accidental cross-container access.
8. References & Community Insights
This issue generates steady community discussion. Here are the resources worth your time:
- Docker Official Docs: Networking with links — The authoritative source on container communication, including why
--linkis deprecated. - Docker Official Docs: Work with bridge networks — Essential reading on the difference between default and user-defined networks.
- Stack Overflow: Container can’t connect to another container on same host — Long-running thread with real-world cases, including the
network_mode: hosttrap. - Reddit r/docker — Weekly posts from people hitting this exact issue; the comment threads often have sharper advice than the docs.
FAQ
Q1: Why can’t two containers on the same Docker host reach each other via DNS?
A1: Both containers must attach to the same user-defined bridge network for Docker’s embedded DNS resolver (127.0.0.11:53) to perform service discovery. If either container sits on the default bridge network, or if they’re on different custom networks, DNS resolution fails. This is by design, not a bug.
Q2: Can containers using host network mode be reached by other containers via DNS?
A2: No. network_mode: host makes the container share the host’s network stack and bypass Docker’s network layer entirely. The embedded DNS resolver can’t see it. The only way to reach it is via the host IP + port mapping, which sidesteps Docker’s service discovery — not recommended for production.
Q3: What’s the performance difference between accessing containers via DNS versus IP?
A3: Negligible. DNS lookups (with local cache hits) take microseconds to low milliseconds, compared to HTTP round-trips that typically run in the hundreds of microseconds to milliseconds range. In our load test at 1,000 QPS, DNS resolution accounted for less than 0.5% of total request latency. Focus on connection pooling instead.
Q4: How do I check what DNS configuration a Docker container is currently using?
A4: Inside the container, run cat /etc/resolv.conf. If you see nameserver 127.0.0.11, the container is using Docker’s embedded DNS. Any other IP suggests custom DNS settings or host networking. Also run docker inspect <container> --format '{{json .NetworkSettings.Networks}}' to confirm the network and IP.
Q5: Can docker-compose’s default network work across multiple compose files?
A5: No. Each docker-compose project creates its own independently-named network (projectname_default). If two services live in different compose files, you must first create an external network (docker network create shared-net), then declare networks: default: external: name: shared-net in both compose files. Otherwise they end up on separate networks and DNS interop fails.