Networking is the thing that trips up most people learning Docker. Container A can’t reach container B, or a container can’t reach the internet, or you expose a port but nothing connects. Most of these problems come down to not understanding how Docker’s networking modes actually work.
I spent a fair amount of time confused about this when I started with Docker. Here’s what I’ve figured out.
The Default Bridge Network
When you run a container without specifying a network, it joins Docker’s default bridge network (docker0). Docker assigns it an IP from the 172.17.0.0/16 range.
docker run -d --name web nginx
docker inspect web | grep IPAddress
Containers on the default bridge can reach each other by IP, but not by name. That’s a limitation of the default bridge — no built-in DNS. To communicate between containers by name, you need a user-defined bridge network.
User-Defined Bridge Networks
Create one:
docker network create myapp
Connect containers to it:
docker run -d --name db --network myapp postgres
docker run -d --name web --network myapp nginx
Now web can reach db by hostname db, and vice versa. Docker’s embedded DNS handles the name resolution automatically.
This is how most Docker Compose setups work by default — Compose creates a network for your project and all services in the same docker-compose.yml can reach each other by service name.
Inspect the network:
docker network inspect myapp
You’ll see all connected containers, their IP addresses, and the network subnet.
Port Publishing
Containers are isolated from the host network by default. To expose a port:
docker run -d -p 8080:80 nginx
8080:80 — bind host port 8080 to container port 80. Traffic to hostip:8080 is forwarded to container:80.
Bind to a specific host interface:
docker run -d -p 127.0.0.1:8080:80 nginx # Only localhost
docker run -d -p 0.0.0.0:8080:80 nginx # All interfaces (default)
Binding to 127.0.0.1 is useful for services that should only be accessible via a reverse proxy — Nginx handles external traffic, and the container is only reachable locally.
Host Network Mode
With host networking, the container shares the host’s network stack directly. No NAT, no port publishing:
docker run -d --network host nginx
The container binds directly to host ports. Nginx on port 80 in the container means port 80 is bound on the host. docker ps won’t show port mappings because there are none.
Benefits: no NAT overhead, better performance, simpler networking for some applications.
Downsides: no isolation. The container can see all host network interfaces. Port conflicts are immediate — if the host already has something on port 80, the container fails to bind.
I use host networking for monitoring tools (like Prometheus node exporter) that need to see host network metrics, and for applications where the performance overhead of bridge NAT matters.
No Network Mode
docker run -d --network none alpine
The container gets a loopback interface only. No external connectivity at all. Useful for processing jobs that shouldn’t touch the network, or for maximum isolation.
Container Network Mode
One container can share another container’s network stack:
docker run -d --name main-app myapp
docker run -d --network container:main-app --name sidecar mysidecar
The sidecar container shares main-app’s network namespace — same IP, same ports, same localhost. Traffic to localhost:8080 in the sidecar hits port 8080 in main-app.
This is the pattern Kubernetes uses for pods — multiple containers sharing a network namespace. In plain Docker, it’s useful for debugging (run a container with network tools attached to a production container’s network) or for sidecar proxy patterns.
Connecting to Multiple Networks
A container can join multiple networks:
docker network create frontend
docker network create backend
docker run -d --name proxy --network frontend nginx
docker network connect backend proxy
docker run -d --name api --network backend myapi
proxy can reach both the frontend and backend networks. api only reaches the backend network. This models a typical three-tier architecture cleanly.
Docker Compose Networking
Compose creates a default network for each project. Every service in a docker-compose.yml automatically joins it:
services:
web:
image: nginx
ports:
- "80:80"
api:
image: myapi
expose:
- "3000"
db:
image: postgres
expose:
- "5432"
All three services can reach each other using their service names: web can connect to api:3000, api can connect to db:5432.
ports publishes to the host (externally accessible). expose only makes the port available to other containers in the same network (not the host). Use expose for internal services — it’s documentation more than functional, since all ports on user-defined networks are reachable by default.
Define custom networks in Compose:
networks:
frontend:
driver: bridge
backend:
driver: bridge
services:
nginx:
networks:
- frontend
api:
networks:
- frontend
- backend
db:
networks:
- backend
This gives you network segmentation within Compose. nginx can’t directly reach db — it must go through api.
Overlay Networks (Swarm)
Overlay networks span multiple Docker hosts. They’re used with Docker Swarm:
docker network create --driver overlay myoverlay
Containers on different hosts can communicate as if they’re on the same network. The overlay driver handles the tunneling (typically VXLAN).
If you’re not using Swarm, you won’t use overlay networks much. For single-host setups, bridge networks are fine. For multi-host, most people use Kubernetes with its own networking (CNI plugins) rather than Swarm these days.
Troubleshooting Common Problems
Container can’t reach the internet — check the container has a working DNS setup. Run docker run --rm alpine nslookup google.com to test. If it fails, your host’s Docker networking might be broken. Try docker network prune and restart Docker.
Containers can’t reach each other by name — they’re on the default bridge or on separate user-defined networks. Move them to the same user-defined network.
Port published but nothing connects — check the container is actually listening on that port. docker exec -it container ss -tlnp. Also verify the host firewall isn’t blocking it: ufw status or iptables -L.
Can’t connect from outside the host — the port might be bound to 127.0.0.1 only. Check docker ps and look at the port binding column.
For applications that need TLS, the typical pattern is to terminate SSL at an Nginx container on the frontend network, while the app container lives on a backend network with no external exposure. The TLS in Docker guide goes into that configuration in detail.