How to Set Up a Custom Bridge for Docker: Network Configuration and Container Connectivity

How to Set Up a Custom Bridge for Docker

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:

  1. No automatic DNS resolution. Containers on docker0 can only reach each other by IP unless you use the legacy and now-deprecated --link flag.
  2. 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.
  3. No per-application address planning. All containers pull from the same /16 regardless 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.name sets the actual ip link interface name.
  • enable_icc controls whether containers on this network can talk to each other directly at all (setting it to false forces all inter-container traffic through published ports only).
  • enable_ip_masquerade controls whether Docker sets up NAT (MASQUERADE) for outbound traffic from this network.
  • driver.mtu matters 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.mtu explicitly 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: true for 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/
Total
0
Shares

Leave a Reply

Previous Post
How to Use pipework to Understand Docker Container Networking

How to Use Pipework to Understand Docker Container Networking: Advanced Network Configuration

Next Post
How to Use OVS with Docker

How to Use OVS with Docker: Open vSwitch Networking and Bridge Configuration Guide

Related Posts