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
- Service listens on 127.0.0.1 inside the container. The classic. The container works, but is unreachable from outside.
- Using the default network. No name resolution, hence hard-coded IPs that change after a restart.
-p 5432:5432instead 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.- Using
localhostinstead of the service name.localhostinside a container means the container itself, not the host. For the host usehost.docker.internal(Docker Desktop) or the bridge address. - 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.
- Host firewall blocking. Docker writes its own
iptablesrules (chainDOCKER). A firewall configuration loaded afterwards can overwrite them. - IPv6 forgotten. Without
--ipv6or proper configuration the service is unreachable over IPv6 — which some clients try first. - 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).