You ran two containers, both are up, and one can't ping the other by name. Nine times out of ten, both containers are sitting on Docker's default bridge network, which deliberately does not resolve container names through DNS. The fix is to put them on a user-defined network, which gives you automatic name-based DNS for free.
The fix: a user-defined bridge network
Create a network, run both containers on it, and use container names as hostnames:
123456789# Create the networkdocker network create my-net# Run two containers attached to itdocker run -d --name web --network my-net nginx:latestdocker run -d --name client --network my-net alpine sleep infinity# Ping by namedocker exec client ping -c 3 web
You should see something like this:
1234PING web (172.18.0.2): 56 data bytes64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.067 ms64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.090 ms64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.075 ms
That's it. Both containers can now resolve each other by container name (web, client) and by any aliases you assign with --network-alias. The names are stable across container restarts, which IP addresses are not.
If the containers are already running and you don't want to recreate them, attach them to the network instead:
12docker network connect my-net existing-container-1docker network connect my-net existing-container-2
Both containers keep their original network attachments. They just gain an additional interface on my-net and can reach each other through it.
Why the default bridge network doesn't work by name
When you run a container without --network, it attaches to a built-in network simply called bridge. On that network, containers can reach each other by IP address, but Docker does not run its embedded DNS server there. To see this for yourself, run a fresh container on the default bridge (under a different name so it doesn't conflict with the web from above) and try to ping it:
12docker run -d --name web-default nginx:latestdocker run --rm alpine ping -c 1 web-default
1ping: bad address 'web-default'
That's intentional. The default bridge exists for backward compatibility with the original Docker networking model from before user-defined networks were a thing. Back then, the way to link containers was the --link flag, which is now legacy and not recommended. If you need name-based DNS, Docker expects you to opt into it by creating a user-defined network.
User-defined networks work because Docker runs an embedded DNS server at 127.0.0.11 inside every container attached to one. You can confirm it's there:
1docker exec client cat /etc/resolv.conf
12nameserver 127.0.0.11options ndots:0
That nameserver knows about every other container on the same network, resolves their names to current IPs in real time, and forwards anything else to the host's upstream DNS. None of this exists on the default bridge.
When ping fails on a user-defined network
If both containers are on the same user-defined network and ping still doesn't work, walk through these checks in order.
- Verify they're actually on the same network. A container can be attached to multiple networks at once, and it's easy to assume two containers share one when they don't:
12docker network inspect my-net \--format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'
12client 172.18.0.3/16web 172.18.0.2/16
1If a container you expected isn't listed, attach it with `docker network connect my-net <container>`.
2. Check whether the image has ping at all. Many minimal images (distroless, scratch-based, or vendor *-slim variants) ship without it. If your container is one of these (call it api here), you'll see this:
1docker exec api ping web
For Alpine derivatives without it, install with apk add iputils-ping. For distroless images you can't install anything, so exec in a debugging sidecar instead:
1docker run --rm --network my-net nicolaka/netshoot ping -c 3 web
The netshoot image bundles ping, nslookup, dig, curl, tcpdump, and most of the tools you'd want for poking at container networking, without polluting your real container.
- Test DNS resolution directly. If
pingcan't resolve the name but the network membership looks right, check whether the embedded DNS server is responding:
1docker exec client nslookup web
123456Server: 127.0.0.11Address: 127.0.0.11:53Non-authoritative answer:Name: webAddress: 172.18.0.2
If nslookup resolves the name but ping doesn't, the issue is ICMP, not DNS. Some hardened hosts drop ICMP between container network namespaces, and some container images strip the CAP_NET_RAW capability that ping needs. Test a real port instead. Alpine's BusyBox ships with nc, so that works directly from client; curl is not in stock Alpine, so either install it with apk add curl or run it from the netshoot sidecar:
12docker exec client nc -zv web 80docker run --rm --network my-net nicolaka/netshoot curl -sI http://web
123HTTP/1.1 200 OKServer: nginx/1.31.1...
For most real debugging, "can the app reach the other service on its port" is the question you actually care about, and ICMP just happened to be the convenient first probe. If the port responds but the application still doesn't behave, the next stop is the container's own logs.
A note on Docker Compose
If you're using Docker Compose, none of this network setup is necessary. Compose creates a user-defined bridge network per project automatically and attaches every service to it. Service names become hostnames inside that network:
12345services:api:image: myapi:latestdatabase:image: postgres:16
From inside the api container, ping database and curl http://database:5432 both resolve correctly. This is the most common reason developers don't hit the default-bridge gotcha until they go back to plain docker run for one-off debugging.
You can inspect the auto-created network the same way as any other:
12docker network lsdocker network inspect <project-name>_default
Compose also aggregates logs from every service into a single stream; see our Docker Compose logs guide for the patterns that work in production.
Common pitfalls
-
Container IPs are not stable. When a container stops, its IP is released back to the network's pool, and the next container to start can claim it. The same applies when you restart a container: the new IP usually won't match the old one. Always reference containers by name, never by an IP you looked up five minutes ago.
-
Different networks are isolated. Containers on different networks can't reach each other, even on the same Docker host. If you ran
webwith--network frontendanddbwith--network backend, they're isolated by design. To bridge them, either attach one container to both networks withdocker network connect, or put them on a shared network from the start. -
Don't use the
--linkflag. It still works on the default bridge, but don't use it. It's been deprecated for years, it only functions on the defaultbridge, and it doesn't survive container restarts cleanly. Any guide recommending it is out of date. -
Use
--network-aliasfor stable hostnames. If you can't change a container's name (maybe your orchestrator generates them), assign a stable alias with--network-alias apiat run time. Other containers can then reach it asapiregardless of the actual container name. This is also useful when you want multiple containers to respond to the same hostname, since Docker will round-robin across them.
Final thoughts
Container-to-container networking has one major quirk (the default bridge behaving differently from user-defined networks) that sends people down hours of debugging when the fix is a one-line docker network create. Once you know to look for it, it becomes a 30-second check.
In production, the harder questions come after the network works: which service called which, how long did it take, where did the request fail. Once you scale past a few containers, docker logs becomes too ephemeral to help and docker stats only gives you a single point in time. That's when you need real observability.
Dash0's infrastructure monitoring ties container metrics together with real-time logs and distributed traces, so you can see exactly which container talked to which, on which port, and where the latency came from.
Start a free trial to see your container traffic, logs, and traces in one view. No credit card required.
