Docker Networking Basics Every Developer Should Know
Docker networking basics come down to how containers talk to each other, to the host, and to the outside world. Every container gets connected to a network the moment it starts, and the driver behind that network decides what it can reach. Get the defaults right and most connectivity problems disappear before they start.
If you only remember one thing: containers on the same user-defined bridge network can reach each other by name, and containers on the default bridge cannot. That single fact explains most of the "why can't my app find the database" tickets.
Docker networking basics: the drivers you'll actually use
Docker ships with several network drivers. For day-to-day work on a single host you'll use three, and you can ignore the rest until you're running a cluster.
bridge
This is the default. When you install Docker it creates a bridge called bridge (also shown as docker0 on the host). Containers attached to it get a private IP on an internal subnet, usually something like 172.17.0.0/16, and reach the internet through NAT on the host.
The catch: the default bridge does not give you automatic DNS between containers. You'd have to link them the old way or use raw IPs, which change on every restart. So you almost never want the default bridge for a real app. You want your own.
# Create a user-defined bridge
docker network create app-net
# Run two containers on it
docker run -d --name db --network app-net postgres:16
docker run -d --name api --network app-net my-api:latest
Now api can connect to the database using the hostname db. Docker runs an embedded DNS server on user-defined networks that resolves container names to their current IP. No IPs to hardcode, no restart surprises.
host
The host driver removes network isolation between the container and the host. The container shares the host's network stack directly, so a service listening on port 8080 in the container is on the host's port 8080 with no publishing needed.
docker run -d --network host nginx
It's faster because there's no NAT layer, and it's handy for things that need to see the host's real interfaces. But you lose port isolation, two host-mode containers can't both grab 8080, and it only behaves this way on Linux. On Docker Desktop for Mac and Windows, host mode does not work the way people expect because the containers run inside a VM.
none
Exactly what it sounds like. The container gets its own network namespace with only a loopback interface and no external connectivity.
docker run --network none alpine ip addr
Use it for batch jobs that process local files and should never touch the network, or when you want to attach networking manually later.
Publishing ports to the outside world
A container on a bridge network is reachable from other containers, but not from your laptop or the internet, until you publish a port. The -p flag maps a host port to a container port.
# host_port:container_port
docker run -d -p 8080:80 nginx
That makes the container's port 80 available on the host's port 8080. Hit http://localhost:8080 and you reach nginx.
A few things worth knowing:
-p 80:80binds on all host interfaces (0.0.0.0). To bind only to localhost, be explicit:-p 127.0.0.1:8080:80. This matters on a public server where you don't want a port exposed to the world.-P(capital) publishes every port the imageEXPOSEs to random high host ports. Fine for quick tests, unpredictable for anything real.EXPOSEin a Dockerfile is documentation. It does not publish anything by itself.
Check what's actually published:
docker port <container>
How containers find each other
On a user-defined bridge, service discovery is just DNS. Container name in, current IP out. Docker Compose leans on this heavily: every service in a Compose file joins the same network and is reachable by its service name.
services:
api:
build: .
environment:
DATABASE_URL: postgres://db:5432/app
db:
image: postgres:16
Here api connects to db by name and never needs an IP. Compose creates a dedicated network for the project automatically, so you get name resolution for free.
You can also attach a container to more than one network, which is how you isolate tiers. Put your database on a backend network, your reverse proxy on a frontend network, and the API on both. Then the database is unreachable from anything on the frontend.
docker network create frontend
docker network create backend
docker network connect backend db
docker network connect frontend proxy
Debugging connectivity when it breaks
When a container can't reach another one, work through this in order. It's the same loop whether you're at your desk or SSHed into a box at 3am.
First, confirm both containers are on the same network:
docker network inspect app-net
The Containers section lists everything attached with its IP. If the container you expect isn't there, that's your answer.
Second, test DNS from inside the container:
docker exec -it api ping -c1 db
# or if ping isn't installed
docker exec -it api getent hosts db
If the name doesn't resolve, you're probably on the default bridge instead of a user-defined one, or the target container isn't running.
Third, test the actual port. Reachability isn't enough. A container can be pingable while the service inside is down or bound to the wrong interface:
docker exec -it api sh -c 'nc -zv db 5432'
A common trap: a service configured to listen on 127.0.0.1 inside its container is unreachable from other containers. It has to bind 0.0.0.0 to accept connections from the network. This bites people with databases and dev servers constantly.
Managing all of this on a remote server usually means a chain of SSH sessions and remembering which host runs what. Docker HQ lets you inspect networks, see published ports, and open a shell into a container from your phone over SSH, which is genuinely useful when an alert wakes you up and your laptop is in another room. If a container or host goes down, it pushes you an alert instead of you finding out from an angry Slack message.
A quick note on Docker Compose networks
Most real projects use Compose, and its default networking covers the majority of cases. One project, one network, service names as hostnames. You override it only when you need to, for example connecting two separate Compose projects:
services:
api:
networks:
- default
- shared
networks:
shared:
external: true
The external: true tells Compose the network already exists and it shouldn't try to create or delete it. Create it once with docker network create shared and multiple projects can join it.
If you're setting Docker up on a home server, network layout is one of the first things worth getting right, and our homelab getting-started guide walks through a sensible default. Once things are running, keeping an eye on them matters as much as wiring them up, which is covered in our guide to Docker container monitoring. And when you start creating and tearing down networks often, stale ones pile up alongside dangling images, so it's worth knowing how to prune safely.
Frequently asked questions
What is the difference between bridge and host networking in Docker?
Bridge networking gives each container its own isolated network namespace with a private IP, and traffic to the outside world goes through NAT. Host networking removes that isolation, so the container shares the host's network stack directly and its ports are the host's ports. Bridge is the safe default for most apps; host trades isolation for a bit of performance and only behaves as expected on Linux.
Why can't my containers reach each other by name?
Almost always because they're on the default bridge network, which has no automatic DNS. Create a user-defined bridge with docker network create and attach both containers to it, or use Docker Compose, which does this for you. Then container names resolve to IPs automatically.
Do I need to publish a port for containers to talk to each other?
No. Containers on the same network reach each other's ports directly using the internal container port, no -p needed. You only publish a port when something outside Docker, like your browser or an external client, needs to reach the container.
What subnet does Docker use, and can I change it?
The default bridge typically uses 172.17.0.0/16. If that clashes with your corporate or VPN network you can override it per network with docker network create --subnet 10.10.0.0/24 my-net, or set a default in /etc/docker/daemon.json under default-address-pools.
Where to go next
Spin up a throwaway user-defined bridge, attach two containers, and confirm one can reach the other by name. That five-minute exercise makes the difference between the default bridge and a real network concrete, and it's the mental model that carries you through Compose, multi-tier isolation, and eventually orchestration. If name resolution works there, you understand the part that trips up most people.