The default docker0 bridge works fine for quick experiments, but the moment I started running anything resembling a real application stack, I ran into its limitations: no built-in DNS-based service discovery between containers, awkward IP address planning, and every container on the host sharing one flat network regardless of what it actually needed to talk to. Custom bridge networks fix all of this, and they’re honestly the single most useful Docker networking feature most people underuse. Here’s how I set them up and why I reach for them by default now.
Why the Default Bridge Falls Short
When Docker starts, it creates docker0 automatically:
ip addr show docker0
docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
Every container launched without an explicit --network flag lands on this bridge. The problems I kept running into:
- No automatic DNS resolution. Containers on
docker0can only reach each other by IP unless you use the legacy and now-deprecated--linkflag. - No network-level isolation. Every container on the host is on the same subnet by default, so a compromised container has network line-of-sight to everything else.
- No per-application address planning. All containers pull from the same
/16regardless of what logical groups they belong to.
User-defined bridge networks solve all three.
Creating a Custom Bridge Network
docker network create --driver bridge my-app-net
Inspect it:
docker network inspect my-app-net
Relevant excerpt of the output:
[
{
"Name": "my-app-net",
"Driver": "bridge",
"IPAM": {
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
}
}
]
Docker automatically picked a free /16 from its internal pool. I can also control this explicitly:
docker network create \
--driver bridge \
--subnet 10.10.0.0/24 \
--gateway 10.10.0.1 \
--ip-range 10.10.0.128/25 \
my-app-net
--ip-range restricts which portion of the subnet Docker actually hands out to containers, which I use when I want to reserve the lower half of the range for statically-addressed infrastructure containers.
Built-In DNS Resolution on Custom Bridges
This is the single biggest reason to use a custom bridge over the default one. On a user-defined network, Docker runs an embedded DNS server that resolves container names automatically — no --link flags, no manual /etc/hosts editing.
docker network create my-app-net
docker run -d --name db --network my-app-net postgres:16-alpine
docker run -d --name api --network my-app-net -e DB_HOST=db myorg/api:latest
From inside api, db resolves automatically:
docker exec api getent hosts db
172.19.0.2 db
Connecting Containers to Multiple Networks
A container isn’t limited to one network. I use this constantly to put a database on a private backend network while exposing an API on a public-facing one:
docker network create backend-net
docker network create frontend-net
docker run -d --name db --network backend-net postgres:16-alpine
docker run -d --name api --network backend-net myorg/api:latest
docker network connect frontend-net api
docker run -d --name web --network frontend-net -p 80:80 myorg/frontend:latest
Now api can reach both db (via backend-net) and web (via frontend-net), but web and db have no direct route to each other at all — this is real network segmentation, enforced by the fact that they simply don’t share a bridge.
docker network inspect frontend-net --format '{{range .Containers}}{{.Name}} {{end}}'
api web
Custom Bridges in Docker Compose
Compose creates a dedicated user-defined bridge network for every project by default, which is why service names already resolve to each other in a typical docker-compose.yml — but I usually make the network topology explicit rather than relying on the implicit default:
version: "3.9"
services:
db:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD=example
networks:
- backend
api:
image: myorg/api:latest
environment:
- DB_HOST=db
networks:
- backend
- frontend
web:
image: myorg/frontend:latest
ports:
- "80:80"
networks:
- frontend
networks:
backend:
driver: bridge
internal: true
frontend:
driver: bridge
Note internal: true on the backend network — this tells Docker not to give that network a route to the outside world at all, so even if db were compromised, it has no path out through that interface.
docker compose up -d
docker compose exec api getent hosts db
172.20.0.2 db
Setting a Custom Bridge Name and Options at the Kernel Level
By default, Docker names the underlying Linux bridge device something like br-<network-id>. Sometimes I want a predictable interface name for monitoring or tc traffic-shaping rules:
docker network create \
--driver bridge \
--opt com.docker.network.bridge.name=br-myapp \
--opt com.docker.network.bridge.enable_icc=true \
--opt com.docker.network.bridge.enable_ip_masquerade=true \
--opt com.docker.network.driver.mtu=1450 \
my-app-net
com.docker.network.bridge.namesets the actualip linkinterface name.enable_icccontrols whether containers on this network can talk to each other directly at all (setting it tofalseforces all inter-container traffic through published ports only).enable_ip_masqueradecontrols whether Docker sets up NAT (MASQUERADE) for outbound traffic from this network.driver.mtumatters a lot on cloud networks with encapsulation overhead (e.g., behind a VPN or overlay) where the default 1500 MTU causes silent packet fragmentation issues.
Verify the interface exists on the host:
ip -d link show br-myapp
Static IP Assignment
Sometimes DNS-based discovery isn’t enough and I need a container pinned to a specific address — for legacy configs or firewall rules keyed on IP:
docker network create --subnet 10.20.0.0/24 static-net
docker run -d --name legacy-app --network static-net --ip 10.20.0.50 myorg/legacy:latest
docker inspect -f '{{.NetworkSettings.Networks.static-net.IPAddress}}' legacy-app
10.20.0.50
Inspecting and Debugging Custom Bridges
docker network ls # list all networks
docker network inspect my-app-net # full JSON detail
docker network prune # remove unused networks
brctl show # (if bridge-utils installed) low-level bridge/port view
ip link show type bridge # kernel-level bridge devices
Common issues:
- Containers can’t resolve each other by name: confirm they’re both on the same user-defined network — the embedded DNS server only works for user-defined networks, not the legacy default bridge.
- “network already exists with a different subnet” errors: happens when you delete and recreate a network with the same name but a different subnet while containers still reference the old network ID — remove dependent containers first, or use
docker network prune. - MTU mismatches causing weirdly slow or hanging connections: usually shows up as SSH or HTTPS sessions that connect but then hang on larger payloads — set
driver.mtuexplicitly to match your underlying network’s real MTU.
Best Practices
- Always use a user-defined bridge network instead of the default one, even for single-container setups — it costs nothing and gives you DNS resolution and cleaner isolation for free.
- Design your network topology the way you’d design VPC subnets: separate networks per tier (frontend/backend/data), and use
internal: truefor anything that shouldn’t have direct internet egress. - Set explicit subnets in shared environments (CI runners, shared hosts) to avoid Docker’s automatic subnet picker colliding with other networks already in use.
- Prune unused networks regularly (
docker network prune) in CI/CD environments where ephemeral networks pile up over time.
Performance and Security Considerations
A few things I keep in mind once custom bridges move from a local experiment to something running real traffic:
NAT overhead. Every packet leaving a bridge network to the outside world passes through Docker’s MASQUERADE iptables rule. For most workloads this overhead is negligible, but for very high-throughput services I’ve occasionally moved to macvlan instead, which gives containers a real MAC on the physical network and skips NAT entirely — at the cost of losing the embedded DNS resolution custom bridges provide.
Container-to-container traffic visibility. Because bridge traffic between containers on the same host never leaves the host’s kernel, it’s invisible to any network-based monitoring (VPC flow logs, TAPs) sitting outside the box. If audit requirements need visibility into service-to-service calls, I rely on application-level tracing or a sidecar proxy rather than expecting network-layer tools to see it.
Firewalling beyond internal: true. The internal: true Compose option is coarse — it’s all-or-nothing for external routing. For finer control over which containers on a network can reach which other containers, I combine custom bridges with explicit iptables rules targeting the bridge interface name I set via com.docker.network.bridge.name, since that gives me a stable interface to write rules against instead of Docker’s auto-generated br-<hash> names.
sudo iptables -I DOCKER-USER -i br-myapp -d 10.10.0.5 -j DROP
Rules added to the DOCKER-USER chain survive Docker restarts, unlike rules added directly to FORWARD, which Docker can rewrite when the daemon reloads its own network rules.
Comparing Custom Bridges to Other Docker Network Drivers
It’s worth knowing when a custom bridge is the wrong tool. Bridges are strictly single-host — if you need containers on different physical or virtual machines to reach each other directly, you want Docker’s overlay driver (with Swarm mode or a similar orchestrator) or one of the tools covered elsewhere in this series, like Weave or a manually built GRE/OVS tunnel. Custom bridges solve single-host multi-tier isolation and discovery; they don’t solve multi-host networking on their own.
Summary
A custom bridge network is the baseline I now use for anything beyond a single throwaway container: it gives you automatic name resolution, real multi-tier network segmentation, control over IP ranges and MTU, and the ability to isolate sensitive services from public-facing ones without touching iptables by hand. If you’re still relying on the default docker0 bridge and --link flags, moving to user-defined bridges is one of the highest-value, lowest-effort upgrades you can make to a Docker deployment.
References
- Docker bridge network driver documentation: https://docs.docker.com/network/drivers/bridge/
- Docker networking overview: https://docs.docker.com/network/
- Docker Compose networking reference: https://docs.docker.com/compose/networking/
- Kubernetes networking model (for comparison as you scale beyond single-host Docker): https://kubernetes.io/docs/concepts/services-networking/