Docker networks are the part that surprises newcomers most often. Containers have their own network interfaces, their own addresses and their own name resolution — and which network a container joins decides whether two containers can see each other and whether a service is reachable from outside.

The network drivers

  • bridge (default): a software network per Docker installation. Containers get addresses from an internal range (typically 172.17.0.0/16) and reach each other by name if they share a user-defined network.
  • host: the container uses the host's network interfaces directly. No separate IP, no port mapping. Fastest option, but no isolation — two containers cannot bind the same port.
  • none: no network beyond loopback. Correct for tasks with no network need and for maximum isolation.
  • overlay: for communication across multiple hosts (Docker Swarm). Requires a control plane.
  • macvlan: the container gets its own MAC address on the physical network and appears there as a separate device. Useful for legacy applications that expect a real network presence.

Why user-defined networks are almost always right

# The default "bridge" network has two major drawbacks:
#   1. NO name resolution between containers
#      (the deprecated --link works one way only)
#   2. All containers share one network

# User-defined network: name resolution included
docker network create app-net

docker run -d --name db  --network app-net postgres:16
docker run -d --name api --network app-net myapi:1.0

# "api" now reaches the database by NAME
#   psql -h db -U app            <- "db" is resolved
#   no IP addresses, no knowledge of internal addressing

# Inspect networks
docker network ls
docker network inspect app-net
docker network inspect app-net --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'

# Attach later
docker network connect app-net some-container
docker network disconnect app-net some-container

# Custom subnet (avoid collisions with your corporate network)
docker network create --subnet 10.99.0.0/24 --gateway 10.99.0.1 internal

Ports: publish or not

# Without -p a container is NOT reachable from outside.
# Its internal IP is only reachable on the Docker host.

# Reachable from the host only (recommended for databases!)
docker run -d -p 127.0.0.1:5432:5432 postgres:16
#   Careful: "-p 5432:5432" without an address binds on ALL interfaces,
#   including the internet if the host is directly exposed!

# Reachable from everywhere (public services only)
docker run -d -p 80:80 -p 443:443 nginx:alpine

# Random host port (handy for testing, useless in production)
docker run -d -p 8080 nginx:alpine

# Internal network only, no port mapping
docker run -d --network app-net postgres:16

# The most common misconception: "EXPOSE 5432" in the Dockerfile
#   EXPOSE publishes NOTHING! It is documentation only.
#   A service becomes reachable exclusively through -p / --publish
#   or because another container in the same network connects to it.

docker port my-container

The path of a request

Internet
   |
   v
Host interface (e.g. 203.0.113.10:443)
   |
   v  Docker proxy rule (DNAT)
Container IP on the bridge network (172.17.0.5:443)

# Consequence: inside the container the application sees the HOST
# address (the bridge is the peer), NOT the real client IP.
# This is why X-Real-IP / X-Forwarded-For headers matter,
# and why everything appears with the same source address in logs.

# With "host" networking there is no translation - the real client
# address passes through. One reason reverse proxies are often
# run in host mode.

Troubleshooting step by step

# 1. Is the container even running?
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker inspect my-container --format '{{.State.Status}} exit={{.State.ExitCode}}'
docker logs --tail 50 my-container

# 2. Which network, which address?
docker inspect my-container --format '{{json .NetworkSettings.Networks}}' | jq
docker inspect my-container --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{$v.IPAddress}}{{"\n"}}{{end}}'

# 3. Is something listening INSIDE the container - and on which address?
#    The most common failure: the service binds to 127.0.0.1 only!
docker exec my-container ss -tlnp
#    "127.0.0.1:8080" -> NOT reachable from outside
#    "0.0.0.0:8080"   -> reachable
#    Configure the app to bind 0.0.0.0
#    (nginx: listen 0.0.0.0:80; node: app.listen(8080, '0.0.0.0'))

# 4. Is the peer reachable from the container?
docker exec -it my-container sh -c 'nc -vz db 5432'
docker exec -it my-container cat /etc/hosts
docker exec -it my-container getent hosts db
#    Empty -> no shared network or wrong name

# 5. Is the port mapping active?
docker port my-container
curl -v http://127.0.0.1:8080/
#    Connection refused -> service not listening on 0.0.0.0 (step 3)

# 6. Check on the host
ss -tlnp | grep -E '8080|:80 |:443'
iptables -t nat -L DOCKER -n | head -20
journalctl -u docker --since '10 min ago' | tail -30

Common mistakes

  1. Service listens on 127.0.0.1 inside the container. The classic. The container works, but is unreachable from outside.
  2. Using the default network. No name resolution, hence hard-coded IPs that change after a restart.
  3. -p 5432:5432 instead of -p 127.0.0.1:5432:5432. Databases accidentally exposed to the internet — one of the most common causes of data leaks with Docker.
  4. Using localhost instead of the service name. localhost inside a container means the container itself, not the host. For the host use host.docker.internal (Docker Desktop) or the bridge address.
  5. Subnet collision. The Docker network uses 172.17.0.0/16. If that range exists in your corporate network, it is unreachable. Set your own range when creating the network.
  6. Host firewall blocking. Docker writes its own iptables rules (chain DOCKER). A firewall configuration loaded afterwards can overwrite them.
  7. IPv6 forgotten. Without --ipv6 or proper configuration the service is unreachable over IPv6 — which some clients try first.
  8. Too many user-defined networks. Every network is a bridge with its own rules. One or two per application is enough: one facing outward, one for internal communication.

A clean production pattern

docker network create --subnet 10.99.1.0/24 app-internal

# Database: internal only, no port mapping
docker run -d --name db \
  --network app-internal \
  --restart unless-stopped \
  -e POSTGRES_PASSWORD_FILE=/run/secrets/db_pass \
  -v db-data:/var/lib/postgresql/data \
  postgres:16

# Application: internal, published only on loopback
docker run -d --name api \
  --network app-internal \
  --restart unless-stopped \
  -p 127.0.0.1:8080:8080 \
  myapi:1.0

# Reverse proxy: published, forwards inward
docker run -d --name proxy \
  --network app-internal \
  --restart unless-stopped \
  -p 80:80 -p 443:443 \
  -v /srv/proxy/conf:/etc/nginx/conf.d:ro \
  nginx:alpine

# Result:
#   Internet -> proxy (80/443) -> api:8080 -> db:5432
#   Database and application are NOT reachable from outside.
#   Only the proxy is attack surface.

Related: Docker, Container, Docker basics, Docker-Netzwerke (DE).