The moment you run more than one container, networking stops being optional. The web app has to find the database. You want to reach the app from your browser. Maybe one container should have no network at all. Docker handles all of this, but the defaults aren’t obvious, and the single most common beginner mistake — expecting containers to find each other by name on the default network — comes straight from not knowing how the pieces fit.
This guide builds the picture from the ground up. You’ll see the three network drivers you’ll actually use, why the default bridge and a user-defined bridge behave differently, how containers resolve each other by name, when you need to publish a port and when you don’t, and how Compose ties it all together so it just works.
By the end you’ll be able to look at a multi-container setup and know exactly how the traffic flows.
Every container gets a network
When Docker starts a container, it attaches it to a network. You don’t have to ask for one — there’s always a default. What changes is which network, and that choice decides three things: whether the container can reach the internet, whether it can reach other containers, and whether the outside world can reach it.
Docker ships with a handful of built-in network drivers. Three matter when you’re starting out:
The network drivers you'll actually use
| bridge | The default. Container gets a private IP on a virtual switch; reaches the internet via NAT; you publish ports to expose it. |
|---|---|
| host | Container shares the host's network stack directly — host IP, host ports, no isolation, no -p needed. Linux-native. |
| none | No networking at all beyond loopback. Fully isolated. |
There are more — overlay for multi-host swarms, macvlan for giving containers real LAN addresses — but you can ignore those until you have a specific reason. Bridge covers the vast majority of single-host work.
List the networks Docker already has:
docker network ls
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
f6e5d4c3b2a1 host host local
1122334455aa none null local
Those three are created for you and can’t be removed. Everything you make sits alongside them.
Bridge: the default, and its one big catch
When you run a container without naming a network, it joins the built-in bridge network:
docker run -d --name web nginx
That container gets a private IP (something like 172.17.0.2), can reach the internet through the host via NAT, and is reachable from the host only on ports you publish. So far so good.
Here’s the catch that trips up nearly everyone. On the default bridge network, containers can reach each other by IP address but not by name. Start a database and an app on the default bridge, point the app at db as a hostname, and it fails — there’s no DNS resolving db to an IP. People conclude Docker networking is broken when it’s working exactly as designed.
User-defined bridges: where DNS by name works
Create your own bridge network and the behavior changes in the way you actually want. Docker runs an embedded DNS server on user-defined networks, so containers resolve each other by name automatically.
Make a network:
docker network create app-net
Run two containers on it:
docker run -d --name db --network app-net postgres:16
docker run -d --name web --network app-net nginx
Now web can reach db using the hostname db, and Docker resolves it to whatever IP that container currently holds. Restart db and get a new IP — the name still resolves. No --link, no IP lookups, no hard-coded addresses.
# From inside the web container, this just works:
docker exec web ping -c1 db
flowchart LR U["Your browser"] -->|localhost:8080 published| W["web container"] W -->|hostname: db| D["db container"] subgraph net["user-defined bridge: app-net"] W D end net -.->|NAT for outbound| I["Internet"]
Host networking: no isolation, no port mapping
The host driver does away with the container’s separate network namespace. The container shares the host’s network stack directly — it uses the host’s IP, and any port it listens on is immediately a port on the host, with no -p mapping involved.
docker run -d --network host nginx
With that, Nginx is listening on the host’s port 80 directly. No -p 80:80, because there’s nothing to map — the container is on the host’s network.
This buys a little performance (no NAT layer) and is handy for tools that need to see the host’s interfaces or open many ports. The cost is isolation: the container has full reach of the host’s network, and a port conflict with a host service is now a direct clash.
None: deliberately offline
The none driver gives a container no networking beyond its own loopback interface. It can’t reach other containers, and it can’t reach the internet.
docker run --rm --network none alpine ip addr
You’ll see only lo. This is niche but real: use it for a job that should be fully sealed off — processing untrusted input, running something you don’t want phoning home, or a batch task that genuinely needs no network. Most containers never use it, but it’s good to know it exists.
Publishing ports vs talking between containers
This is the distinction that clears up most confusion, so it’s worth stating plainly. There are two completely separate kinds of communication:
Container to container, on the same network. This needs no port publishing at all. Containers on a shared network reach each other directly on the container’s own ports. If db listens on 5432, then web connects to db:5432 — done. The port is never published to the host.
Host or outside world to a container. This is what -p is for. Publishing maps a host port to a container port so traffic from your browser, or from another machine, can get in:
docker run -d --name web --network app-net -p 8080:80 nginx
Here 8080:80 means “host port 8080 forwards to container port 80.” Your browser hits localhost:8080; the container still listens on 80 internally.
The common mistake is publishing ports that don’t need to be published. A database that only your app talks to should stay on the shared network with no -p line at all. Publishing it exposes it to the host and possibly the whole LAN for no reason.
Publishing vs internal communication
| Container → container (same network) | No -p needed. Use the container/service name and its real port, e.g. db:5432. |
|---|---|
| Browser/host → container | Publish with -p host:container, e.g. -p 8080:80. |
| Internal-only service (db, cache) | Stay on the shared network, do not publish. Reachable by name already. |
| Local-only published port | Bind to 127.0.0.1, e.g. -p 127.0.0.1:5432:5432, to keep it off the LAN. |
Connecting and inspecting networks
You can attach a running container to another network, or detach it, without recreating it:
# Attach an existing container to a network
docker network connect app-net web
# Detach it again
docker network disconnect app-net web
A container can sit on several networks at once, which is how you isolate tiers — a web container on both a public-facing network and a private backend network, with the database only on backend.
To see exactly what’s on a network and which IPs are assigned, inspect it:
docker network inspect app-net
That prints the subnet, the gateway, and every connected container with its IP — the quickest way to confirm two containers really are on the same network when name resolution isn’t working.
How Docker Compose handles all this for you
Here’s the good news for anyone using Compose: it does the right thing automatically. When you run docker compose up, Compose creates a user-defined bridge network for the project and attaches every service to it. That’s why services reach each other by name with zero extra configuration — the embedded DNS comes free with the network Compose builds.
So in a Compose file, a web service connects to a db service using db as the hostname, because the service name is the DNS name:
services:
web:
image: nginx
ports:
- "8080:80" # published to the host
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: change-me
# no ports: — only 'web' needs to reach it, by name, on the shared network
Notice db has no ports: block. It doesn’t need one — web reaches it as db:5432 on the network Compose created. Only web is published, because only web needs to be reached from your browser. That’s the publishing-vs-internal rule applied in practice.
You can define extra networks explicitly when you want to separate tiers:
services:
web:
image: nginx
networks: [frontend, backend]
db:
image: postgres:16
networks: [backend] # database is unreachable from the frontend network
networks:
frontend:
backend:
Now web straddles both networks, but db lives only on backend, so nothing on frontend can touch the database directly. The Docker Compose beginner guide walks through a full working stack if you want to see services, volumes, and networking together.
A mental model you can rely on
Docker networking, the short version
- Every container joins a network; the default is the built-in bridge
- Default bridge: containers reach each other by IP only — no DNS by name
- User-defined bridge: containers resolve each other by name (use this for multi-container apps)
- Same network = direct communication, no -p needed
- -p is only for exposing a container to the host/outside world
- Don't publish internal services; bind local-only ones to 127.0.0.1
- host = share the host stack, no isolation; none = no network at all
- Compose makes a user-defined network for you, so names just work
Wrapping up
Most of Docker networking comes down to a few rules that reinforce each other. Containers always get a network; the default bridge gives you internet access but no name resolution; a user-defined bridge adds DNS so containers find each other by name; and publishing a port is a separate concern from containers talking among themselves. Get those straight and the rest — host mode, none, multi-network isolation — slots in around them.
If you take one habit away, make it this: create a user-defined network (or let Compose do it) for anything with more than one container, and only publish the ports that genuinely need to face the outside. From here, two good next steps are putting a reverse proxy in front of your containers so several apps share one set of ports, and learning how persistent data works in the Docker volumes guide. For more, browse the Docker & Containers guides.