Server

Container Networking: bridge, host, none, and Custom Networks

By Prabath Thalangama· October 24, 2026· 3 min read
#docker#networking#containers

Introduction

Docker networking confuses people because a lot happens invisibly: a bridge interface, veth pairs, NAT rules, an embedded DNS server. Understanding the modes makes port and connectivity problems obvious.

The modes

bridge (default). Each container gets a private IP on the docker0 bridge (or a user-defined bridge). Outbound traffic is NATed (masqueraded) to the host's IP. Inbound requires explicit port publishing.

host. The container shares the host's network namespace — no isolation, no NAT, no port publishing needed (or possible). EXPOSE 8080 means it's on the host's 8080 directly. Fastest, least isolated. Linux only.

none. No networking at all except loopback. For batch jobs that only touch the filesystem, or where you attach networking yourself.

container:<name>. Share another container's namespace (how a sidecar shares localhost with the main container; also how Kubernetes pods work).

Default bridge vs user-defined bridge

The legacy default bridge network has no service discovery — containers reach each other only by IP, and you'd use the deprecated --link. A user-defined bridge adds an embedded DNS server: containers resolve each other by name.

docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name api --network appnet -p 3000:3000 myapi
# inside "api", the host "db" resolves to db's container IP

Always create a network. Compose does this for you automatically (one network per project, services reachable by service name).

Publishing vs exposing

  • EXPOSE 8080 in the Dockerfile is documentation only — it opens nothing.
  • -p 8080:80 (or ports: in compose) publishes: Docker adds a DNAT rule so the host's 0.0.0.0:8080 forwards to the container's :80.
  • -p 127.0.0.1:8080:80 binds only to loopback — the container port is reachable from the host but not the network. Use this for admin/debug ports.

The firewall reality: -p 8080:80 inserts rules into the DOCKER iptables chain that bypass your INPUT filtering. A ufw deny 8080 will not block a published container port. Mitigations: bind to 127.0.0.1, use DOCKER-USER chain rules, or set "iptables": false and manage it yourself (advanced).

Inspecting

docker network ls
docker network inspect appnet          # subnet, gateway, connected containers + IPs
docker inspect -f '{{json .NetworkSettings.Networks}}' api | jq
docker exec api ip addr
docker exec api getent hosts db        # test embedded DNS
iptables -t nat -L DOCKER -n           # the DNAT rules for published ports

Verification and troubleshooting

  • Container A can't reach container B by name — they're on the default bridge (no DNS) or on different networks. docker network connect appnet B, or put both on the same user-defined network.
  • Published port not reachable from another machine — the app inside binds to 127.0.0.1 inside the container, not 0.0.0.0. It must listen on all interfaces within its namespace.
  • ufw/firewalld rule not blocking a container port — Docker's NAT rules run first. Use DOCKER-USER chain or bind to loopback.
  • Outbound DNS fails inside the container — the container uses Docker's resolver (127.0.0.11) which forwards to the host's; if the host's /etc/resolv.conf points at 127.0.0.53 (systemd-resolved) it can break. Set --dns or fix compose dns:.
  • host mode "port already in use" — no isolation, so the container's port collides with a host service. That's the tradeoff.
  • Two compose projects can't talk — separate networks by default. Create an external shared network and reference it in both.
  • Slow DNS / 5s stalls — musl libc (alpine) doing parallel A/AAAA lookups against Docker's resolver; a known interaction. single-request options or a glibc base.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.