The Default Bridge Network in Depth
A close look at docker0 and Docker's default bridge: how packets flow, which iptables rules the daemon programs, where it diverges from user-defined bridges, and how to migrate.
Published on • October 16, 2026
AI Assistant

Every container you start without --network lands on the same network: the built-in bridge network, backed by the docker0 Linux bridge on the host. Docker’s own docs are blunt about it: user-defined bridge networks are superior to the default bridge network, and the default one is considered a legacy detail that is not recommended for production. That does not make it magic, though. The default bridge is just a bridge device, a subnet, a set of firewall rules, and a handful of daemon flags. This post walks through all four so you know exactly what you are running when you type docker run nginx.
What docker0 actually is
When the Docker daemon starts for the first time on Linux, it creates a single built-in network called the default bridge. Under the hood that means a Linux bridge device named docker0 with an address from Docker’s default address pool (the built-in default starts at 172.17.0.0/16), plus per-container plumbing:
+-------------------- container --------------------+
| eth0 172.17.0.2/16 default route via 172.17.0.1
+---------|----------------------------------------+
| veth pair
+---------v--------- host -------------------------+
| vethXXXXXX <--> docker0 (172.17.0.1/16) <--> eth0 (uplink)
+---------------------------------------------------+
Each container gets one end of a veth pair; the other end is attached to docker0. Outbound packets are forwarded by the host and masqueraded so that devices outside the host only ever see the Docker host’s IP address. Inbound access to unpublished ports is blocked by firewall rules the daemon installs for you.
You can watch this live:
$ docker run -dit --name alpine1 alpine ash
$ docker network inspect bridge
$ ip -br addr show docker0
docker0 UP 172.17.0.1/16
$ ip -br addr show # inside the container: eth0 is 172.17.0.2/16
The daemon exposes a set of bridge driver options (com.docker.network.bridge.enable_ip_masquerade, com.docker.network.bridge.enable_icc, com.docker.network.driver.mtu, and others). Several have direct dockerd equivalents: --ip-masq, --icc, --ip, --mtu. The bridge device itself is configurable with --bridge, which is how you run multiple daemon instances on one host. For the default bridge network specifically, configuration happens through daemon.json (bip, fixed-cidr, mtu, dns, default-gateway, ipv6, bip6, fixed-cidr-v6) and requires a Docker restart.
The concrete differences from a user-defined bridge
No embedded DNS on the default bridge
Containers attached to a user-defined network use Docker’s embedded DNS server at 127.0.0.11, so ping db just works. Containers on the default bridge get a copy of the host’s /etc/resolv.conf instead and cannot resolve each other by name at all:
# inside alpine1, on the default bridge
/ # ping -c1 alpine2
ping: bad address 'alpine2'
/ # ping -c1 172.17.0.3 # IP works fine
PING 172.17.0.3: 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.064 ms
The historical workaround is the legacy --link flag, which has to be created in both directions and gets painful past two containers. It also has a side effect people still rely on: linked containers share environment variables. On user-defined networks that mechanism is gone, replaced by better options (a shared volume, Compose environment blocks, or Swarm secrets and configs).
Weaker isolation
Every container without an explicit --network joins the same default bridge. Unrelated stacks can therefore reach each other by IP address. A user-defined network gives you a scoped segment where only the containers you attached can talk. Related: containers on the same bridge effectively expose all ports to each other, while crossing between different bridge networks only works through published ports.
Runtime flexibility
You can docker network connect and docker network disconnect a running container against a user-defined network. Removing a container from the default bridge means stopping it and recreating it with different network options. Same story for configuration: each user-defined network is created and configured with docker network create, while the default bridge shares one global set of settings (MTU, iptables behavior) changed outside Docker itself.
Static IP plumbing
With a user-defined network you pin addresses at creation time:
$ docker network create --subnet 192.0.2.0/24 my-net
$ docker run -d --network my-net --ip 192.0.2.69 nginx
On the default bridge you are stuck with whatever Docker allocates from fixed-cidr, and you cannot disable IPv4 address configuration there (you can on a user-defined network with --ipv4=false).
One operational limit worth knowing: bridge networks become unstable and inter-container communication may break when 1000 or more containers connect to a single network, a kernel-level limitation tracked in moby/moby#44973.
The firewall rules Docker programs
On Linux, Docker creates firewall rules to implement isolation, port publishing, and NAT. The docs are explicit that you should not modify these rules, because bridge networks do not work without them. The default backend is iptables; nftables is supported through the firewall-backend daemon option, and for bridge networks both provide the same functionality.
Three jobs are being done for you:
- Masquerading (POSTROUTING): outgoing packets from containers have their source address rewritten to a host address. You can turn this off per user-defined network with
-o com.docker.network.bridge.enable_ip_masquerade=false, or replace masquerade with a fixed SNAT source viacom.docker.network.host_ipv4/com.docker.network.host_ipv6. - Port publishing:
docker run -p 8080:80installs a NAT rule mapping a host address and port to the container’s port. Default gateway mode isnat; alternatives arenat-unprotected,routed, andisolated. - Inter-container isolation: the bridge driver installs rules so containers on different bridge networks can only communicate using published ports. ICC itself is toggled by
com.docker.network.bridge.enable_icc(defaulttrue,dockerd --icc).
Docker also enables net.ipv4.ip_forward (and IPv6 forwarding) at startup and sets the default forwarding policy to drop so your host does not accidentally become an open router. Set "ip-forward-no-drop": true in daemon.json if you actually need routing.
To see it yourself:
$ sudo iptables -t nat -S | grep -E 'MASQUERADE|DOCKER'
$ sudo iptables -S DOCKER
$ sudo nft list ruleset # when the nftables backend is active
$ cat /proc/sys/net/ipv4/ip_forward
Representative output (exact rules vary by version and backend):
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
Two sharp edges follow from this. First, publishing a port with plain -p 8080:80 binds 0.0.0.0, which the port publishing docs call insecure by default; use -p 127.0.0.1:8080:80 or set com.docker.network.bridge.host_binding_ipv4 (or "ip": "127.0.0.1" for the default bridge) to keep services local. Second, ufw and Docker are effectively incompatible: Docker routes container traffic in the nat table before packets reach the INPUT and OUTPUT chains ufw uses, so published ports ignore your ufw rules. Disabling iptables for Docker is worse: without replacement rules, containers lose masqueraded internet access while all of their ports become reachable from the local network.
Inspecting the default bridge from the host
Three views together tell you almost everything: what Docker thinks, what the kernel has, and what the firewall permits.
$ docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}} gw {{.Gateway}}{{end}}'
172.17.0.0/16 gw 172.17.0.1
$ docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'
alpine1 172.17.0.2/16
alpine2 172.17.0.3/16
$ ip -br link show docker0
docker0 UP 02:42:c2:5a:1b:04
$ bridge link | grep docker0 # or: brctl show
$ ip route
default via 192.168.1.1 dev eth0
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
Inside a container, ip addr show shows the veth end with its allocated address, and cat /etc/resolv.conf confirms you are on the default bridge (a copy of the host’s resolver file) rather than pointed at 127.0.0.11. If containers cannot reach each other, check that forwarding is on and that you did not hand-edit Docker’s rules:
$ cat /proc/sys/net/ipv4/ip_forward
1
$ sudo iptables -S | head -20
Useful one-liners when triaging connectivity: docker network inspect bridge for addresses, docker exec <c> ip route for the container’s view of the gateway, and iptables -t nat -S to confirm the masquerade and DNAT rules for published ports are still present.
When the default bridge is still acceptable
Use it, provisionally, when all of these hold: a single app on the host, no need for service names (you hardcode IPs or use --add-host), no need to attach or detach networks at runtime, and nobody else on the box cares which containers talk to each other. Throwaway demos, one-off docker run debugging, and CI helpers all live happily on docker0. Adding --link or editing /etc/hosts inside containers is the signal that you have outgrown it.
Migrating off it
The migration is boring, which is the point:
$ docker network create app-net
$ docker network connect app-net existing-container # or add to compose/compose file
$ docker run -d --name api --network app-net my/api # new containers: always pass --network
Then delete the legacy links (--link flags) and replace IP literals with container names. If you had tuned daemon.json for the default bridge, move those knobs to per-network options: bip becomes --subnet, fixed-cidr becomes --ip-range, MTU becomes -o com.docker.network.driver.mtu, and the default bind address becomes -o com.docker.network.bridge.host_binding_ipv4. Finally, confirm isolation held:
$ docker network inspect app-net
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
e9261a8c9a19 app-net bridge local
17e324f45964 bridge bridge local
Key Takeaways
- The default bridge is
docker0plus a172.17.0.0/16-style subnet; every unspecified container lands there and can reach every other one by IP. - No embedded DNS: name resolution only exists on user-defined networks (
127.0.0.11); the default bridge relies on IP addresses or legacy--link. - Docker programs NAT, masquerade, DNAT for published ports, and inter-container isolation rules itself; do not hand-edit them, and do not expect ufw to filter container traffic.
- Publishing with plain
-pbinds all interfaces; pin to127.0.0.1for local-only services. - The default bridge is fine for demos and single-app hosts; anything with more than one service should move to a user-defined bridge with
docker network create.