Key Takeaways
- 90% of Docker inter-container DNS failures happen because both containers aren’t attached to the same user-defined bridge network.
- The default
docker0bridge network only supports IP-based communication, not automatic container name DNS resolution. docker run --linkis a deprecated hack you should stop using; the correct approach isdocker network create+--networkassignment.- Community posts on Reddit r/homelab show most failures stem from mixing
hostnetwork mode or forgetting custom network gateway settings. - One simple command —
docker exec -it <container> cat /etc/resolv.conf— tells you immediately if the DNS config has gone sideways.
1. How This Problem Shows Up: A Real-World Failure
Friday night. I’m doom-scrolling Reddit when I hit a post on r/homelab: “Issue accessing two containers via DNS on same docker.” The comment section was a dumpster fire — people arguing about --link, others screaming “use docker-compose,” and that one guy who always says “just use Kubernetes.”
My blood pressure spiked. I’ve seen this exact failure pattern too many times. Three months ago on a three-node Docker Swarm cluster, I had AdGuardHome and Portainer running on the same Ubuntu VM. AdGuardHome could ping Portainer’s IP just fine, but portainer as a hostname? Dead. Took us three hours to realize they were on different networks — one on bridge, the other on host.
You think that’s the only trap? Keep reading.
2. How Docker Container DNS Actually Works
2.1 The Embedded DNS Server
Since Docker 1.10, every Docker daemon runs an embedded DNS server listening on 127.0.0.11:53. But here’s the kicker — it doesn’t activate for every container.
The cardinal rule: The container must be attached to a user-defined network.
When a container connects to a custom network, Docker rewrites its /etc/resolv.conf to point nameserver to 127.0.0.11. Docker’s resolver handles all .local domain queries. Container names get registered as A records, and other containers on the same network can resolve them by name.
2.2 Default Bridge vs. User-Defined Bridge: The Fundamental Difference
I’m pasting this table because I’m tired of explaining it to people who say “but both containers are on the same bridge”:
| Feature | Default Bridge (docker0) | User-Defined Bridge |
|---|---|---|
| Auto DNS by container name | ❌ Not supported | ✅ Supported |
| Communication via IP | ✅ Supported | ✅ Supported |
--link manual aliasing | ✅ Supported (deprecated) | ❌ Not needed |
| Network isolation | ❌ All containers can talk | ✅ Configurable |
| DNS injection | ❌ Uses host DNS | ✅ Uses embedded DNS (127.0.0.11) |
| Hot-plug containers | ❌ Requires restart | ✅ Works at runtime |
The root cause is simple: you’re not on a custom network, so the DNS resolution doesn’t exist.
3. Root Cause Analysis + Diagnostic Commands
3.1 The Debug Workflow
Here’s exactly what I run when I hit this problem:
# 1. Check which networks each container is on
docker inspect <container1> | jq '.[].NetworkSettings.Networks | keys'
docker inspect <container2> | jq '.[].NetworkSettings.Networks | keys'
# 2. Verify /etc/resolv.conf to confirm DNS target
docker exec <container1> cat /etc/resolv.conf
# Expected: nameserver 127.0.0.11
# Bad: nameserver 8.8.8.8 or 192.168.1.1 (external DNS)
# 3. Test DNS resolution directly
docker exec <container1> ping <container2>
docker exec <container1> nslookup <container2> 127.0.0.11
# If nslookup fails, it's a network-level issue
# 4. Inspect the custom network membership
docker network ls
docker network inspect <network_name>
3.2 Dissecting the r/homelab Failure
The Reddit user’s setup looked like this:
- Pi-hole → running on a VM (not a container)
- Nginx Reverse Proxy → LXC container
- Docker VM containing:
- AdGuardHome container
- Portainer container
The problem? Both containers were started with docker run, but one used --network host to grab the host’s network stack. Once a container uses host networking, Docker never injects 127.0.0.11 into its /etc/resolv.conf. Portainer had no idea who 127.0.0.11 was, so it went to the host’s DNS servers to look up adguardhome — and found nothing.
3.3 The Hidden Asymmetry Trap
Someone on Hacker News (in the 227-point Codex exclusion issue thread) described a nastier variant: two services started via docker-compose up without an explicit networks: block. Docker-Compose creates a default network (project_default) for all services. But if one service has network_mode: host, it gets kicked out of that default network. The result? The host-mode service can’t resolve other containers by name, but those containers can reach the host-mode service via localhost.
This asymmetric access pattern is the most common thing I see people miss during debugging.
4. The Fix: Step-by-Step Resolution
4.1 Solution 1: Unified Custom Network (Recommended)
# Step 1: Create a user-defined bridge network
docker network create --driver bridge --subnet 172.22.0.0/16 my_app_net
# Step 2: Start containers on that network
docker run -d --name adguardhome --network my_app_net \
-p 53:53/tcp -p 53:53/udp -p 784:784/udp -p 853:853/tcp \
-p 3000:3000/tcp \
adguard/adguardhome
docker run -d --name portainer --network my_app_net \
-p 8000:8000 -p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
portainer/portainer-ce
# Step 3: Verify DNS resolution
docker exec adguardhome nslookup portainer 127.0.0.11
# Output: Name: portainer, Address: 172.22.0.3
4.2 Solution 2: Docker Compose (Cleaner)
version: '3.8'
services:
adguardhome:
image: adguard/adguardhome
container_name: adguardhome
networks:
- app_network
ports:
- "53:53/tcp"
- "53:53/udp"
- "784:784/udp"
- "853:853/tcp"
- "3000:3000/tcp"
portainer:
image: portainer/portainer-ce
container_name: portainer
networks:
- app_network
ports:
- "8000:8000"
- "9443:9443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
app_network:
driver: bridge
ipam:
config:
- subnet: 172.22.0.0/16
Run it: docker-compose up -d
4.3 Solution 3: Fixing Host Network Mode Containers
If your use case demands host networking (e.g., capturing all network interface traffic), you need to manually add the container back to a custom network:
# Start with host mode, then attach to custom network
docker run -d --name adguardhome --network host adguard/adguardhome
docker network connect my_app_net adguardhome
# Now adguardhome has both host AND custom network access
# Other containers on my_app_net can reach it by name
# But adguardhome needs to use the custom network IP to reach others
Caveat: /etc/resolv.conf still shows external DNS. You’ll need to either manually set DNS or use --dns on the custom network.
5. Performance and Security Trade-offs
5.1 Embedded DNS vs. External DNS
| Metric | Docker Embedded DNS (127.0.0.11) | External DNS (e.g., 8.8.8.8) |
|---|---|---|
| Container name resolution | ✅ Supported | ❌ Not supported |
| Resolution latency | ~0.5ms (loopback) | ~10-30ms (network round-trip) |
| Scalability | Single-node only | Cross-node capable |
| Cache policy | TTL-based, default 600s | Depends on upstream |
| Failure mode | All container DNS breaks | External access breaks, internal still works |
There’s a nasty edge case here. If your host’s /etc/resolv.conf has a search domain (like search example.com), containers inherit it. When you query a container name, Docker’s embedded DNS tries containername, then containername.example.com. If your external DNS can resolve containername.example.com to some random IP, you get a false positive — everything looks like it’s working, but you’re talking to the wrong host.
5.2 The “Gateway Hijack” Problem
Someone in the Reddit thread suggested “attaching other containers to the DNS stack’s bridged network and explicitly setting its gateway.” This points to a more insidious bug: if the custom network’s gateway is set to another container’s IP (like Pi-hole’s IP), DNS requests get routed to that container, which then forwards them back in a loop.
Fix: always set an explicit gateway:
docker network create \
--driver bridge \
--subnet 172.22.0.0/16 \
--gateway 172.22.0.1 \
my_app_net
6. Alternatives and Trade-offs
6.1 Docker Compose links and depends_on
links has been deprecated since Docker Compose V3, but people still use it. It writes static entries to /etc/hosts:
services:
app:
image: myapp
links:
- db:database
Problem: container rebuilds change IPs, but /etc/hosts doesn’t update automatically. If service startup order shifts, DNS breaks silently.
6.2 Nginx Reverse Proxy with Unix Sockets
Some people use Nginx as a reverse proxy over Unix sockets instead of network ports. Better performance, more secure — but both containers must be on the same host, and the config is gnarly:
upstream backend {
server unix:/var/run/docker.sock:/var/run/portainer.sock;
}
6.3 Just Use Kubernetes Already?
For two containers? That’s like using a flamethrower to light a candle. But if you’ve got 10+ services that need service discovery, K8s’s CoreDNS is rock-solid compared to Docker’s built-in DNS. The cost: a massive control plane overhead, a brutal learning curve, and at least 2GB RAM reserved for K8s components.
7. Summary
Docker container DNS failures boil down to two causes:
- Containers aren’t on the same user-defined network.
hostnetwork mode prevents DNS injection.
The fix is trivially simple:
- Create a custom network → assign containers at startup → done.
- Don’t use
--link. - Don’t expect container name resolution to work automatically in host mode.
If you’re still hitting this, run docker inspect to check the network config, then cat /etc/resolv.conf to confirm the DNS target. Ten minutes of methodical checking and you’ll have it nailed.
References & Community Insights
- Reddit r/homelab: Issue accessing two containers via DNS on same docker — Real user config post-mortem
- Docker Official Docs: Legacy container links — Why –link was deprecated
- Docker Official Docs: User-defined bridge networks — Custom network official guide
- Docker Embedded DNS Source Code (Go) — If you want to see how it works under the hood
FAQ
Q: Two containers on the same host can’t communicate by container name. Why?
A: Because they aren’t connected to the same user-defined bridge network. The default docker0 bridge doesn’t support automatic DNS resolution by container name. Create a custom network (docker network create) and specify it with --network at container startup.
Q: Does the --link flag fix DNS resolution?
A: Temporarily, but --link has been deprecated since Docker 1.9. It writes static entries to /etc/hosts that don’t update when container IPs change on restart. The recommended solution is user-defined networks.
Q: Why does Docker’s embedded DNS use 127.0.0.11? A: Docker’s embedded DNS server listens on 127.0.0.11:53. The 127.0.0.11 address is a reserved loopback IP in Docker’s network namespace, allowing containers to reach the DNS service via the loopback interface without external network dependencies.
Q: Why does DNS break when a container uses --network host?
A: In host network mode, the container shares the host’s network stack. Docker doesn’t modify /etc/resolv.conf and doesn’t inject 127.0.0.11 as the nameserver. Fix it by running docker network connect to attach the container to a custom network after startup.
Q: How do I verify Docker container DNS is working?
A: Run docker exec <container> nslookup <target-container> 127.0.0.11 or docker exec <container> getent hosts <target-container>. If it returns an IP address, DNS is working. If it returns “not found” or a timeout, your network config is wrong.
