LabHub
배우기 러닝패스 코스

Volumes, Networks and Compose

How Do Containers Find Each Other

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

네트워크 네임스페이스는 인터페이스와 라우팅 테이블만이 아니라 루프백 주소와 포트 번호 공간까지 통째로 복제합니다. 그래서 컨테이너의 127.0.0.1 은 호스트의 127.0.0.1 과 완전히 다른 주소입니다.

Concept map: 그 컨테이너 자신 · 기본 브리지 네트워크에서는 컨테이너 이름으로 서로를 찾을 수 없습니다. · 애플리케이션의 바인딩 주소 · 모든

왜 이게 필요했나

초보자가 밟는 세 가지 증상은 전부 이 한 문장에서 설명됩니다.

  1. 브라우저에서 연결이 거부된다
  2. 옆 컨테이너에서 curl http://api:3000 이 이름을 못 찾는다
  3. 컨테이너에서 curl http://127.0.0.1:5432 로 호스트 DB 에 못 붙는다

세 번째가 특히 헷갈립니다. 컨테이너 안에서 127.0.0.1 로 접속하면 그 컨테이너 자신을 가리킵니다. 호스트가 아닙니다.

어떻게 동작하나

기본 브리지 네트워크에서는 컨테이너 이름으로 서로를 찾을 수 없습니다.

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

사용자 정의 네트워크를 만들어 붙이면 됩니다. 차이는 컨테이너 안의 /etc/resolv.conf 에 그대로 드러납니다 — 네임서버가 컨테이너 네임스페이스 안에만 존재하는 주소로 잡히고, 그 요청이 런타임의 리졸버로 넘어갑니다. 리졸버는 같은 네트워크에 있는 컨테이너의 이름과 별칭을 알고 있어 답해 줍니다. 기본 브리지는 하위 호환을 위해 남아 있는 것이고, 실무에서는 항상 사용자 정의 네트워크를 만들어 씁니다.

두 번째로 중요한 것은 애플리케이션의 바인딩 주소입니다.

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

이 소켓에는 호스트에서 들어온 패킷이 절대 닿지 않습니다. 패킷은 eth0 주소로 도착하기 때문입니다. 정답은 0.0.0.0:3000 입니다. "0.0.0.0 은 위험하지 않느냐"는 반론이 있는데, 컨테이너 안에서의 0.0.0.0 은 그 컨테이너의 네트워크 네임스페이스 전체를 뜻할 뿐이고 외부 노출 여부는 퍼블리싱 바인딩 주소가 결정합니다. 두 계층을 섞으면 둘 다 틀립니다.

현장에서 만나는 모습

포트 퍼블리싱의 기본값은 호스트의 모든 인터페이스입니다.

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

첫 줄은 서버가 공인 IP 를 갖고 있다면 인터넷에 데이터베이스를 연 것과 같습니다. 그리고 호스트 방화벽으로 막았다고 안심하면 안 됩니다 — 컨테이너로 향하는 패킷은 호스트가 최종 목적지가 아니라서 일반적인 INPUT 규칙을 지나지 않고, 그보다 앞선 단계에서 이미 목적지가 바뀝니다. 가장 안전하고 단순한 해법은 애초에 바인딩 주소를 제한하는 것입니다.

컨테이너끼리만 통신하는 서비스는 아예 퍼블리싱하지 않는 것이 맞습니다. 그리고 컨테이너를 여러 네트워크에 동시에 붙일 수 있다는 점도 기억해 두세요. 프론트엔드 네트워크와 백엔드 네트워크를 나누고 앱만 양쪽에 소속시키면, DB 는 외부에서 아예 보이지 않게 됩니다.

네트워크 종류와 쓰는 자리

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

기본 bridge 와 사용자 정의 bridge 가 다릅니다. 기본 네트워크에서는 이름으로 서로를 못 찾고 IP 를 알아야 합니다. 사용자가 만든 네트워크에는 내장 DNS 가 붙어 컨테이너 이름이 곧 호스트명 이 됩니다.

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     # 이름으로 닿는다

그래서 실무에서는 언제나 네트워크를 하나 만들어 씁니다. compose 는 이것을 자동으로 해 줍니다.

포트 매핑이 하는 일과 안 하는 일

-p 8080:80호스트의 8080 을 컨테이너의 80 으로 잇는 NAT 규칙 입니다. 컨테이너끼리는 이 매핑과 무관하게 컨테이너 포트로 직접 통신합니다.

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

이것을 헷갈려 컨테이너 안에서 localhost:8080 을 부르는 실수가 흔합니다. 컨테이너의 localhost자기 자신 입니다. 다른 컨테이너를 부르려면 이름을 씁니다.

-p 127.0.0.1:8080:80 처럼 주소를 붙이면 외부에 노출되지 않습니다. 개발용 데이터베이스를 열 때 이 습관이 사고를 막습니다 — -p 5432:5432 는 방화벽이 없으면 인터넷에 DB 를 여는 것입니다.

호스트에서 컨테이너 안을 들여다보기

컨테이너에 sstcpdump 가 없어도 호스트에서 그 네임스페이스로 들어갈 수 있습니다.

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 은 네트워크 네임스페이스만 들어간다는 뜻이라, 호스트의 도구를 그대로 쓰면서 컨테이너의 네트워크를 봅니다. distroless 이미지를 진단하는 가장 확실한 방법입니다.

다음 실습에서 할 것

사용자 정의 네트워크를 만들어 이름으로 서로를 찾게 하고, 다른 네트워크의 컨테이너가 정말 보이지 않는지 확인한 뒤, 루프백에만 포트를 퍼블리싱해 봅니다.