Why two containers on the same host can’t reach each other by default

Start Postgres and your API with plain docker run and the API cannot reach the database, even though both containers sit on the same machine.

1
2
docker run -d --name db postgres:16
docker run -d --name api myapp

Both land on Docker’s default bridge network, where every Docker container goes unless you say otherwise. They get IP addresses. What they don’t get is any way to find each other by name, so the API would have to hardcode the database’s internal IP — an address that changes every time the container restarts.

A Docker network fixes that. It’s a virtual switch containers attach to, with its own IP range and its own DNS. Create one yourself and container names become hostnames, and you decide which containers can see each other.

Which network driver to use: bridge, host, or overlay

Docker ships several built-in network drivers. Three of them cover almost everything you’ll hit on a single host or a swarm:

DriverScopeContainers get their own IPName-based DNSTypical use
bridge (default)single hostyesno (IP only)what you get with no --network flag
bridge (user-defined)single hostyesyesthe default choice for local dev and single-host deployments
hostsingle hostno — shares the host’s network stackn/ahigh-throughput services, tools that need the host’s real network view
overlaymultiple hosts (swarm)yesyesservices split across more than one Docker host

There’s also none, which gives a container a loopback interface and nothing else. Useful for a batch job that has no business on the network at all.

Every fresh docker run lands on the default bridge. It works, but it only routes by IP; the old --link flag existed to paper over the missing names, and Docker marks it legacy. A user-defined bridge does the same job with DNS built in. That’s the one you want.

How to create a Docker network and attach containers to it

1
docker network create app-net

That’s a private bridge network with its own subnet, isolated from the default bridge and from every other user-defined network you create. Attach containers at start time:

1
2
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name api --network app-net -p 8080:8080 myapp

Inside api, connecting to db:5432 works. No IP lookup, no environment variable juggling: Docker’s embedded DNS server resolves the container name db to its current address on app-net, and updates that mapping when the container restarts with a new one. Docker Compose runs on the same mechanism. docker compose up creates a network for the stack, and every service reaches its siblings by service name.

You can also attach a container that’s already running, without restarting it:

1
2
docker network connect app-net some-container
docker network disconnect app-net some-container

A container can sit on more than one network at once. That’s how you give a service a public-facing network and a private one shared only with the backend containers behind it.

Why publishing a port is not how two containers reach each other

-p 8080:8080 maps the host’s port 8080 to the container’s, so you can hit the API from your browser at localhost:8080. It says nothing about whether api can reach db. Traffic between containers on the same user-defined network stays internal, published ports or not.

Publish only the ports something outside the Docker host has to reach. A database that talks to nothing but the API doesn’t need -p 5432:5432: leave it unpublished and neither the host nor the internet can connect to it directly. api still reaches it, because they share app-net.

When to use –network host, and when not to

1
docker run --rm -d --network host --name my_nginx nginx

With --network host the container skips its own network namespace and binds straight to the host’s interfaces. No NAT, no bridge, no port mapping: -p and -P are ignored, and Docker prints a warning if you pass them. That drops a small amount of per-packet overhead and lets the container see the host’s real network interfaces, which is what network monitoring tools and software needing a large contiguous port range are after.

It also drops the isolation a bridge gives you. The container can bind any port the host isn’t already using, and it can see everything on the host’s network. Linux supports this fully. Docker Desktop for Mac and Windows needs it turned on explicitly (available from Docker Desktop 4.34), because the Docker VM’s networking isn’t the host’s. Windows containers don’t support it at all. For most services, a user-defined bridge with published ports does the same job with less exposure.

When you need an overlay network instead of a bridge

Bridge and host networks stop at the edge of one Docker host. Run Docker Swarm across several machines and a service on host A needs an overlay network to reach a service on host B:

1
docker network create -d overlay --attachable app-overlay

An overlay encapsulates container traffic and routes it between the Docker daemons on each swarm node, so containers on different physical or virtual machines resolve each other by name exactly as they would on a bridge. The --attachable flag lets standalone containers join it too; without it, only swarm services can connect. If you’re not running swarm or another multi-host orchestrator, skip this driver. A single-host bridge network covers Compose stacks and local setups.

How to inspect and debug a Docker network

1
docker network ls

Every network on the host, including the bridge, host, and none that Docker always creates.

1
docker network inspect app-net

The subnet, the gateway, and every container currently attached with its IP address on that network. This is the first place to look when one container can’t reach another. Read the Containers section: if one of them is missing, it isn’t on this network, and docker network connect puts it there.

To test DNS resolution from inside a running container:

1
docker exec api getent hosts db

An IP back means name resolution works and the problem is in the application — wrong port, or the service isn’t listening yet. Nothing back means the two containers aren’t on the same network. Confirm that with docker network inspect, or ask each container what it’s attached to:

1
docker inspect <container> --format '{{ .NetworkSettings.Networks }}'

Create one network per stack and put every container on it

The default bridge is what you get for free. It isn’t what you should build on. Run docker network create <app>-net for each application stack, attach every container that stack needs, and save --network host for the rare service that genuinely needs the host’s own network stack.

Once you’re hand-managing more than a couple of containers, stop doing it by hand. Docker Compose creates the network and attaches the containers for you from a single YAML file. Docker volumes cover the other half of surviving a restart: the data those containers write.