You start two containers. You run docker exec container-a ping container-b. Nothing. You check the docs, add --link, which works but is deprecated. Two hours later you discover the actual problem: you're using the default bridge.
Two bridges, very different behaviors
Docker ships with a default bridge network called bridge. Every container you start without specifying a network joins it. They can ping each other by IP. They cannot ping each other by name — DNS resolution doesn't exist on the default bridge. It's a legacy behavior frozen in place.
User-defined bridge networks are different. When you create one with docker network create my-net and connect containers to it, Docker runs an embedded DNS server that resolves container names to IPs automatically. ping container-b from container-a works. That's the entire difference.
What the default bridge actually does
All containers on the default bridge share the subnet 172.17.0.0/16 by default. Docker assigns IPs sequentially from .2 upward. You can reach 172.17.0.3 from 172.17.0.2. You can't reach container-b from container-a because there's no name-to-IP mapping.
The --link flag was the original workaround — it injected an entry into /etc/hosts on the source container. It works. It's also unidirectional, doesn't survive container restarts with new IPs, and was officially deprecated in Docker 1.12. Don't use it.
User-defined networks: what you actually want
Create a network, attach your containers to it, done:
docker network create app-net
docker run -d --name postgres --network app-net postgres:16
docker run -d --name api --network app-net my-api-imageFrom inside api, postgres resolves correctly. The embedded DNS handles it. Containers can also use network aliases:
docker run -d --name postgres-primary \
--network app-net \
--network-alias db \
postgres:16Now both postgres-primary and db resolve to the same container. Useful when you want application code to use a stable name regardless of the container's actual name.
Subnet isolation between networks
User-defined networks are isolated from each other by default. Containers on net-a can't reach containers on net-b unless a container is attached to both, or you explicitly route between them. This is how you segment a Compose stack — frontend talks to API, API talks to database, frontend can't reach the database directly.
In Compose, you can restrict which services talk to which:
services:
api:
networks: [frontend, backend]
db:
networks: [backend]
nginx:
networks: [frontend]
networks:
frontend:
backend:Nginx can reach api. Api can reach db. Nginx cannot reach db. The network graph enforces this at the kernel level — no application-layer firewall required.
Port mapping vs internal networking
Port mapping (-p 5432:5432) is for traffic from outside Docker — your laptop, the public internet, services not in the same Docker network. Between containers on the same user-defined network, use the container port directly, not the mapped one. Your API should connect to postgres:5432, not localhost:5432.
If your API container is trying to connect to localhost:5432 and it works in some configurations but not others, check whether host networking (--network host) is accidentally enabled. With host networking the container shares the host's network namespace — localhost works but you lose all isolation.
Connecting a running container to a network
You don't have to recreate containers to change network membership:
docker network connect app-net existing-container
docker network disconnect bridge existing-containerUseful for debugging — attach a temporary diagnostic container to a network:
docker run --rm -it --network app-net nicolaka/netshoot nslookup postgresnetshoot has dig, nslookup, curl, tcpdump, and everything else you'd want for container network diagnosis without installing tools in your application images.
What Compose does by default
Every Compose project creates a default user-defined bridge network named <project>_default. All services join it. Service names resolve as hostnames. This is why docker compose up works without explicit network configuration in most cases — Compose does the user-defined network setup for you automatically.
The limitation: the default network has no extra configuration. If you need MTU adjustments, custom subnets, or IPAM settings, define networks explicitly in your compose file and configure them there.
Building something? builds.anethoth.com lists indie SaaS projects with transparent metrics. More at anethoth.com.