bridge — the default, isolated virtual network
docker run -d --name web nginx # attaches to the default bridge network automatically
docker network create my-network # or create a custom, user-defined bridge network
docker run -d --network my-network --name web nginx
Creates a private, virtual network on the host. Containers on the same bridge network can reach each other by IP — and, on a user-defined bridge specifically, by container name via DNS (see the DNS question) — and reach the outside world via NAT through the host. This is the right default for the overwhelming majority of single-host container setups.
host — no network isolation at all
docker run -d --network host nginx
The container shares the host's network namespace directly — no isolation, no virtual interface, no port mapping needed (a container binding to port 80 with --network host is genuinely binding to port 80 on the host itself, directly). This eliminates a layer of network translation overhead, which occasionally matters for latency-sensitive or high-throughput networking use cases. But it sacrifices the isolation benefit entirely. Port conflicts between containers become a real, direct concern — two containers can't both bind the same host port — and a compromised container has direct access to the host's network stack.
none — no networking beyond loopback
docker run --network none myapp
The container gets no external network interface at all — only its own internal loopback (127.0.0.1). Useful for workloads that genuinely need zero network access, either for isolation/security reasons (a batch job processing only local files, with no legitimate reason to reach the network at all) or as an extra layer of defense-in-depth.
overlay — connecting containers across multiple hosts
docker network create -d overlay my-overlay-network # used with Docker Swarm
Extends bridge-like networking across multiple Docker hosts, letting containers on different physical or virtual machines communicate as if they were on the same local network. This is specifically what Docker Swarm mode (see the production topic) uses to let services span multiple nodes while still reaching each other by name.
macvlan — a container appears as a physical device
docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 my-macvlan
Assigns each container its own MAC address and effectively makes it appear as a distinct physical device directly on the host's physical network, bypassing Docker's usual NAT-based bridge networking entirely. Used for niche cases where legacy applications or network monitoring tools expect containers to look like real, individually-addressable devices on the network, rather than being hidden behind the host's single IP via NAT.
Choosing between them, in practice
| Driver | Typical use case |
|---|---|
| bridge (user-defined) | The default choice for nearly all single-host multi-container applications |
| host | Performance-sensitive networking, or single-purpose hosts running one dominant service |
| none | Workloads that should have no network access at all |
| overlay | Multi-host Swarm deployments |
| macvlan | Legacy applications or tooling that specifically requires containers to appear as distinct physical network devices |
For the large majority of everyday Docker usage — a web application talking to a database, a handful of services communicating with each other on one machine — a user-defined bridge network (not even the default bridge network, for reasons covered in that question) is the right choice, and the most common one by a wide margin.
Related Resources
What happens without specifying a network
docker run -d --name web nginx
docker run -d --name api myapi:1.0
Without an explicit --network flag, both containers attach to Docker's default bridge network (visible as the docker0 interface on the host). Each gets its own private IP address on this virtual network, and can reach the outside internet through NAT via the host.
The critical limitation: no built-in DNS resolution by name
docker exec web ping api
# ping: api: Name or service not known
On the default bridge network specifically, containers cannot resolve each other by container name. You'd have to look up api's current IP address manually (docker inspect api) and hardcode that IP into web's configuration. This is fragile, since a container's IP on the default bridge can change if it's stopped and restarted.
The fix: a user-defined bridge network
docker network create my-app-network
docker run -d --network my-app-network --name web nginx
docker run -d --network my-app-network --name api myapi:1.0
docker exec web ping api
# PING api (172.20.0.3): 56 data bytes <- resolves correctly by name!
A user-defined bridge network (created explicitly via docker network create) includes Docker's embedded DNS server automatically. This lets containers on that same network resolve each other by container name, or by any --network-alias assigned. This is the single biggest practical reason the default bridge is discouraged for real multi-container applications.
Additional benefits of user-defined networks over the default
- Better isolation. You can create multiple separate user-defined networks. Containers only see or reach others explicitly attached to the same network, giving you a deliberate segmentation tool — for example, a "frontend" network and a "backend" network, with only specific containers bridging both — that the single, shared default bridge doesn't provide.
- Dynamic reconnection — a running container can be connected to or disconnected from a user-defined network on the fly (
docker network connect/disconnect), without needing to restart it; the default bridge is more rigid about this. - This is exactly what Docker Compose does automatically. Every
docker-compose.ymlproject automatically gets its own user-defined bridge network by default. This is precisely why Compose services can reference each other by their service name out of the box (see the Compose topic), without any manual network setup at all.
The default bridge is really only still relevant for the simplest, single-container, no-inter-container-communication case, or for historical/legacy setups. Anything with more than one container talking to each other should get a user-defined network instead.
Related Resources
What -p actually does
docker run -d -p 8080:80 nginx
This maps port 8080 on the host to port 80 inside the container — a request to http://<host-ip>:8080 gets forwarded to whatever is listening on port 80 inside the container (in this example, nginx's default port).
docker run -d -p 80 nginx # publishes container port 80 to a RANDOM available host port
docker run -d -p 127.0.0.1:8080:80 nginx # only binds to the host's loopback interface, not all interfaces
Omitting the host port lets Docker pick a random available one (visible via docker port <container>). Specifying a host IP restricts which network interface the mapping is bound to — useful for only exposing a port locally on the host itself, without making it reachable from the broader network.
The mechanism: iptables DNAT rules
When you publish a port, Docker automatically inserts iptables rules into the host's netfilter configuration. Specifically, this is a DNAT (Destination NAT) rule that rewrites the destination of incoming packets, redirecting traffic arriving at the host's published port to the container's actual internal IP and port on its Docker network.
Incoming request: <host-ip>:8080
│
▼ (iptables DNAT rule, inserted automatically by Docker)
Container's internal IP:80 (on the bridge network)
This is conceptually similar to how Kubernetes's kube-proxy uses iptables (or IPVS/eBPF) rules to route Service traffic to backing Pods (see that stack's kube-proxy question). Docker's port publishing solves an analogous problem — getting external traffic to the right internal destination — using the same underlying Linux networking primitive.
Why unpublished ports still work between containers
docker network create my-network
docker run -d --network my-network --name api myapi:1.0 # port 3000, NOT published to the host
docker run -d --network my-network --name web mywebapp:1.0
api's port 3000 isn't reachable from the host machine or the internet at all (no -p flag was used). But web, being on the same Docker network, can still reach api:3000 directly. This is because containers on the same network can always reach each other's container-internal ports without any publishing needed. Port publishing (-p) is specifically about making a container's port reachable from outside the Docker network entirely — from the host, or from the internet if the host itself is internet-facing. It is not a requirement for container-to-container communication within the same network.
Publishing all exposed ports at once
docker run -P myapp # capital -P: publish every port listed in the Dockerfile's EXPOSE
# instructions, each to a random host port
Recall from the Dockerfile topic that EXPOSE alone is just documentation or metadata. -P (capital, distinct from lowercase -p) is what actually turns each EXPOSEd port into a real, published mapping, to a randomly chosen host port each. It reads the image's own declared EXPOSE list, rather than requiring you to specify each mapping manually. An internal-only service (a database only ever accessed by other containers on the same network) should typically not publish a port at all, reducing its exposed attack surface. Only the actual entry-point service usually needs one reaching the host or internet.
Related Resources
The embedded DNS server
docker network create my-network
docker run -d --network my-network --name db postgres:16
docker run -d --network my-network --name api myapi:1.0
docker exec api cat /etc/resolv.conf
# nameserver 127.0.0.11
Every container attached to a user-defined network automatically has its DNS resolver pointed at 127.0.0.11 — Docker's own embedded DNS server, running as part of the Docker daemon's networking implementation. Application code inside api can simply connect to db by name:
// Inside the "api" container
const client = new Client({ host: 'db', port: 5432 }); // resolves via Docker's embedded DNS
Why this survives container restarts, even with a changing IP
docker restart db
docker inspect db --format='{{.NetworkSettings.Networks.my_network.IPAddress}}'
# a possibly DIFFERENT IP than before the restart
Because api connects to db by name, not by hardcoded IP, this restart is transparent to api. The next DNS lookup for db simply returns whatever db's current IP is. The application code never needed to know or care that the underlying IP changed. This is precisely the same fundamental need Kubernetes Services and CoreDNS solve at cluster scale (see that stack's Service and DNS questions). The problem — ephemeral container IPs shouldn't be hardcoded into configuration — and the solution shape — a name-based DNS layer resolving to current IPs — are conceptually identical. They just operate at the scale of one Docker host versus an entire cluster.
Network aliases — additional names for the same container
docker run -d --network my-network --network-alias database --name db postgres:16
const client = new Client({ host: 'database' }); // resolves to the "db" container, via its alias
A container can be given one or more additional aliases beyond its own container name. This is useful when you want application configuration to reference a more generic, stable name (database) that isn't tied to the specific container's actual name, since that name might change between deployments or environments.
Compose does all of this automatically
services:
db:
image: postgres:16
api:
image: myapi:1.0
environment:
- DB_HOST=db # "db" resolves automatically -- Compose creates a user-defined
# network and names containers after their service name
This is exactly why Docker Compose services can reference each other by service name with zero manual network setup. Compose automatically creates a user-defined bridge network for the whole project and names each container after its service. So the DNS-based resolution described above just works out of the box.
What this DNS mechanism doesn't do
Docker's embedded DNS only resolves names within the same user-defined network. A container on a completely different Docker network has no visibility into this DNS namespace at all. There's also no automatic load balancing built into this basic mechanism the way a Kubernetes Service provides. If multiple containers share the same name or alias, or you need actual load-balanced traffic distribution across multiple instances of a service, that's a capability Docker Compose, Swarm, or a real orchestrator like Kubernetes provides — not something the base DNS mechanism alone does for you. Hardcoding a container's IP instead of its name reintroduces exactly the fragility this mechanism exists to eliminate. It should be flagged in code review on sight.
Related Resources
This question directly builds on the earlier default-bridge question — here's a fuller comparison, focused specifically on what user-defined networks add.
Capability comparison
| Default bridge | User-defined bridge | |
|---|---|---|
| Containers reachable by IP | Yes | Yes |
| Containers reachable by name (DNS) | No | Yes |
| Can create multiple isolated networks | No (one shared default) | Yes (create as many as needed) |
| Dynamically connect/disconnect a running container | Limited | Yes, freely |
| Used automatically by Docker Compose | No | Yes (Compose creates one per project) |
Isolation and segmentation via multiple networks
docker network create frontend-net
docker network create backend-net
docker run -d --network frontend-net --name web nginx
docker run -d --network backend-net --name db postgres:16
docker run -d --network frontend-net --name api myapi:1.0
docker network connect backend-net api # api bridges BOTH networks
This setup gives web no path to reach db directly at all (different networks, no shared connectivity) — only api, deliberately connected to both frontend-net and backend-net, can bridge between them. This is a genuine, deliberate isolation and segmentation tool. You can architect which containers are allowed to even see which others, rather than everything sharing one flat, undifferentiated network the way the single default bridge works.
Dynamic network membership
docker network connect my-network already-running-container
docker network disconnect my-network already-running-container
A container can be attached to or detached from a user-defined network while it's still running, without needing to stop and recreate it. This is useful for reconfiguring connectivity on the fly — for example, temporarily attaching a debugging container to a production network segment for a one-off diagnostic session, then detaching it afterward. It's a flexibility the default bridge doesn't offer in the same way.
Why Compose's automatic behavior reinforces this as the standard practice
services:
web:
image: nginx
api:
image: myapi:1.0
docker compose up
# Creates a network named "<project-name>_default" automatically,
# and attaches both "web" and "api" to it -- giving them DNS-based
# discovery of each other by service name with ZERO explicit
# network configuration required.
Compose is the most common way people run multi-container Docker setups locally, by a wide margin. The fact that it does this automatically, by default, for every project, reflects just how strongly the ecosystem has converged on "always use a user-defined network, never the default bridge" as the correct baseline practice.