Container Networking Explained: Bridges, Ports, and Why Your Containers Cannot Talk

Container Networking Explained: Bridges, Ports, and Why Your Containers Cannot Talk

Container networking looks simple until two containers cannot see each other, or a service you thought was firewalled turns out to be reachable from the whole network. Both have straightforward explanations.

The default bridge, and why not to use it

Containers started without a network option join the default bridge. They get an address on a private subnet and can reach the internet through NAT.

What they cannot do is resolve each other by name. The default bridge has no DNS, so curl http://database:5432 fails and you are left using IP addresses that change on restart.

Create a network instead:

docker network create backend
docker run -d --name db --network backend postgres:17
docker run -d --name app --network backend myapp

Now db resolves from inside app automatically. This one change fixes most container-to-container problems, and it is why every Compose file works: Compose creates a user-defined network for you.

docker network ls
docker network inspect backend

Publishing ports

docker run -d -p 8080:80 nginx

Host port on the left, container port on the right. Traffic to host port 8080 reaches port 80 in the container.

The important detail is what the host binds to. -p 8080:80 binds 0.0.0.0, meaning every interface, so the service is reachable from your entire network and from the internet if the machine is exposed.

# Reachable from everywhere
docker run -d -p 8080:80 nginx

# Reachable only from the host
docker run -d -p 127.0.0.1:8080:80 nginx

The second form is what you want for anything sitting behind a reverse proxy, which is most self-hosted software.

Why your firewall does not stop it

This surprises people and it is important.

Docker publishes ports by inserting its own rules ahead of the input chain your firewall manages. A ufw deny or a plain nftables input rule does not apply, because the packet is being forwarded rather than delivered locally.

You can configure the DOCKER-USER chain to filter it, and the reliable answer is simpler: bind to 127.0.0.1. A service that is not listening on an external interface cannot be reached from one, regardless of firewall rules.

Check what is actually exposed:

sudo ss -tlnp | grep -v 127.0.0.1
docker ps --format '{{.Names}}\t{{.Ports}}'

Anything in the first list that you did not intend is worth fixing now.

Reaching the host from a container

localhost inside a container is the container. To reach the host:

# Docker, on Linux with the flag
docker run --add-host=host.docker.internal:host-gateway myapp

# Podman
# host.containers.internal is available by default

# Or the bridge gateway address
docker network inspect bridge | grep Gateway

Network modes

bridge is the default and what you want almost always.

host removes the network namespace entirely. The container shares the host stack, binds ports directly, and has no isolation. Useful for performance-sensitive networking or services needing broadcast and mDNS, such as Home Assistant discovery. It removes a boundary, so use it deliberately.

none gives no networking at all, which is right for a batch job that only touches mounted files.

container:name shares another container’s namespace, which is how a sidecar pattern works and how Kubernetes Pods share a network.

Rootless is different

Rootless containers cannot create bridges, because that needs privileges. They use a userspace stack instead, pasta on current Podman or slirp4netns previously.

It works transparently. It is slower for high-throughput traffic, and source addresses can appear differently to services that care, which occasionally confuses logging and rate limiting.

Debugging

# Get a shell with network tools in the container's namespace
docker run --rm -it --network container:myapp nicolaka/netshoot

# Or from the host, enter the namespace
sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' myapp) -n ss -tlnp

That second command uses namespaces directly and works even when the container image has no networking tools in it, which minimal images usually do not.

Frequently Asked Questions

Why can my containers not reach each other by name?

Because they are on the default bridge network, which has no DNS resolution between containers. Create a user-defined network and attach both, and each container becomes reachable by its name automatically.

Why is my published port reachable despite my firewall?

Docker inserts its own rules ahead of yours when publishing a port, so ufw or a plain nftables input rule does not block it. Bind to 127.0.0.1 in the port mapping, which is the reliable fix regardless of firewall.

What does the colon in 8080:80 mean?

Host port on the left, container port on the right. So 8080:80 means traffic arriving on host port 8080 reaches port 80 inside the container. Getting the order backwards is the most common port mapping mistake.

How does a container reach the host?

Through the gateway address of its bridge network, or via host.docker.internal on Docker and host.containers.internal on Podman where supported. Referring to localhost inside a container means the container itself, not the host.

What does network mode host actually do?

It removes the network namespace, so the container shares the host network stack directly. Ports bind on the host with no mapping and no isolation. Useful for performance and for services needing broadcast or mDNS, and it removes a security boundary.

Do rootless containers network differently?

Yes. They use a userspace network stack such as pasta or slirp4netns rather than kernel bridging, because creating bridges needs privileges. It works transparently and is slower for high-throughput traffic.