TT Lab
Get started
Learn Learning paths Courses

Volumes, Networks and Compose

How Do Containers Find Each Other

Continue in TT Lab

One-line summary

A network namespace duplicates not only the interfaces and routing table but the loopback address and the port number space as a whole. So a container's 127.0.0.1 is a completely different address from the host's 127.0.0.1.

Why this is needed

The three symptoms beginners run into are all explained by this one sentence.

  1. The connection is refused in the browser
  2. From the neighboring container, curl http://api:3000 cannot find the name
  3. From a container, curl http://127.0.0.1:5432 cannot reach the host DB

The third is especially confusing. If you connect to 127.0.0.1 from inside a container, it points to that container itself. It is not the host.

How it works

On the default bridge network, containers cannot find each other by container name.

docker run -d --name db postgres:16
docker run --rm alpine:3.20 ping -c1 db
# ping: bad address 'db'

You just create a user-defined network and attach the containers to it. The difference shows up right away in the /etc/resolv.conf inside the container — the nameserver is set to an address that exists only inside the container namespace, and the request is passed on to the runtime's resolver. The resolver knows the names and aliases of containers on the same network and answers. The default bridge remains only for backward compatibility, and in practice you always create and use a user-defined network.

The second important thing is the application's binding address.

docker exec api ss -tlnp
# LISTEN 0 511 127.0.0.1:3000 ...

Packets coming in from the host never reach this socket. This is because the packets arrive at the eth0 address. The right answer is 0.0.0.0:3000. There is an objection, "isn't 0.0.0.0 dangerous?", but 0.0.0.0 inside a container only means the whole network namespace of that container, and whether it is exposed externally is decided by the publishing binding address. If you mix the two layers up, you get both wrong.

What it looks like in the field

The default for port publishing is all interfaces of the host.

docker run -d -p 5432:5432 postgres:16            # 위험
docker run -d -p 127.0.0.1:5432:5432 postgres:16  # 안전

If the server has a public IP, the first line is the same as opening the database to the internet. And do not feel safe just because you blocked it with the host firewall — packets heading to a container do not have the host as their final destination, so they do not pass through the ordinary INPUT rules, and their destination is already rewritten at an earlier stage. The safest and simplest solution is to restrict the binding address in the first place.

For a service that only containers talk to, it is right not to publish it at all. Also remember that you can attach a container to several networks at once. If you split a frontend network and a backend network and make only the app a member of both, the DB becomes completely invisible from outside.

Network types and where they are used

bridge  (기본)   가상 스위치. 컨테이너끼리 사설 IP 로 통신
host             호스트 네트워크를 그대로 씀. 격리 없음
none             네트워크 없음. 계산만 하는 작업
container:<이름>  다른 컨테이너의 네임스페이스 공유

The default bridge and a user-defined bridge are different. On the default network, containers cannot find each other by name and you have to know the IP. On a network you created, built-in DNS is attached, and the container name becomes the hostname.

docker network create app-net
docker run -d --name db --network app-net postgres:16
docker run --rm --network app-net alpine ping -c1 db     # 이름으로 닿는다

So in practice you always create one network and use it. compose does this automatically.

What port mapping does and does not do

-p 8080:80 is a NAT rule that connects port 8080 of the host to port 80 of the container. Containers communicate with each other directly on the container port, regardless of this mapping.

호스트에서 → localhost:8080
다른 컨테이너에서 → web:80        ← 8080 이 아니다

It is a common mistake to confuse this and call localhost:8080 from inside a container. A container's localhost is itself. To call another container, use its name.

If you attach an address as in -p 127.0.0.1:8080:80, it is not exposed externally. When opening a development database, this habit prevents accidents — -p 5432:5432 opens the DB to the internet if there is no firewall.

Looking inside a container from the host

Even if the container has no ss or tcpdump, you can enter its namespace from the host.

PID=$(docker inspect -f '{{.State.Pid}}' myapp)
nsenter -t $PID -n ss -ltnp          # 컨테이너의 리스닝 포트
nsenter -t $PID -n tcpdump -i any -c 20 port 80

-n means entering only the network namespace, so you look at the container's network while using the host's tools as they are. It is the surest way to diagnose a distroless image.

What you will do in the next lab

You will create a user-defined network so that containers find each other by name, check that containers on a different network really cannot be seen, and try publishing a port only on the loopback.