-
0.0.0.0에 listen한다는 것과 네임스페이스의 포트 — 컨테이너는 호스트의 포트를 나눠 쓰지 않는다IT 2026. 9. 16. 21:00

▶ 동영상 개요 — 0.0.0.0에 listen한다는 것과 네임스페이스의 포트 — 컨테이너는 호스트의 포트를 나눠 쓰지 않는다
7분 59초 — 포트 테이블은 네트워크 네임스페이스마다 통째로 따로 존재한다 — 호스트와 컨테이너가 같은 0.0.0.0:8099를 동시에 listen해도 커널이 네임스페이스를… — NotebookLM 동영상 개요로 생성 🎧 오디오 개요 — 0.0.0.0에 listen한다는 것과 네임스페이스의 포트 — 컨테이너는 호스트의 포트를 나눠 쓰지 않는다
24분 31초 — 포트 테이블은 네트워크 네임스페이스마다 통째로 따로 존재한다 — 호스트와 컨테이너가 같은 0.0.0.0:8099를 동시에 listen해도 커널이 네임스페이스를…. 화면은 이 글의 인포그래픽 한 장 고정 — NotebookLM 오디오 개요로 생성 서버 코드를 짜다 보면
listen("0.0.0.0", 8080)같은 줄을 아무 생각 없이 쓴다. 그런데 어느 순간 이게 정확히 무슨 뜻인지 물으면 말문이 막혔다. 0.0.0.0은 IP처럼 생겼는데 실제로 존재하는 기계 주소는 아니다. "특정 포트를 열어둔다"는 것도 그렇다 — 나는 그동안 호스트로 들어온 네트워크 패킷이 목적지 포트 번호를 보고 곧장 호스트의 그 포트에 연결되는 것이라고 이해하고 있었다. 여기까지는 그럭저럭 맞다.문제는 도커였다. 컨테이너를 띄우면 도커가 네트워크 네임스페이스(network namespace)라는 격리된 네트워크 공간을 새로 만든다. 그러면 포트는 어떻게 되는 걸까. 그 네임스페이스만을 위한 포트가 다시 열리는 것인가, 아니면 호스트와 포트 번호를 공유하는 것인가? 이 질문이 계속 걸렸다. 그래서 Docker 29.2.1과 alpine 이미지로 직접 두드려 봤다. 결론부터 말하면 포트 테이블은 네임스페이스마다 통째로 따로 존재한다 — 공유가 아니라 완전한 별개다. 이 글은 그 결론에 이르기까지, 먼저 0.0.0.0의 정체를 확인하고, 이어서 포트가 네임스페이스마다 어떻게 갈라지는지 실험으로 확인한 기록이다.
0.0.0.0은 카드가 아니라 "이 안의 모든 문"이다
한 대의 기계는 IP 주소를 여러 개 가진다. 자기 자신만 부르는
127.0.0.1(loopback, 소프트웨어 가상 인터페이스), 물리 랜카드에 붙은 주소(예:192.168.0.10), 그리고 도커를 깔면 생기는 브리지 주소(172.17.0.1)까지. 이 주소들은 각각 하나의 네트워크 인터페이스에 붙어 있다 — 인터페이스는 패킷이 드나드는 문이라고 보면 된다.프로세스가 소켓을 만들어 서버를 열 때는 이 문들 중 어디로 오는 요청을 받을지 시작 시점에 고른다. 그 고르는 행위가 bind다. 그런데 문이 여러 개일 때 "전부 다 받겠다"를 어떻게 표현하나? 문마다 소켓을 하나씩 열 수도 있지만 번거롭다. 그래서 리눅스는 특별한 값 하나를 예약해 뒀다 — 그게
0.0.0.0, 커널 내부 상수 이름으로는INADDR_ANY다. 어느 특정 카드의 주소가 아니라 "지금 이 네임스페이스에 존재하는 모든 인터페이스"를 한 번에 가리키는 와일드카드다.위 다이어그램은 bind가 "어느 문으로 오는 요청을 들을지 고르는 일"임을 보여준다. 서버 프로세스에서 세 갈래로 나뉘는데, 각 갈래가 bind할 수 있는 대표적인 세 선택지다.
127.0.0.1을 고르면 loopback 문 하나로만 귀를 대는 것이라 같은 기계 안에서만 닿고 바깥에서는 영영 못 들어온다.0.0.0.0을 고르면 현재 네임스페이스에 있는 문을 전부 여는 것이라 외부에서도 들어올 수 있다. 특정 카드 주소를 콕 집으면 그 카드로 들어온 패킷만 받는다. 여기서 이미 중요한 단서가 하나 나온다 — 0.0.0.0의 "모든"은 온 세상의 모든 문이 아니라 "지금 이 네임스페이스에 보이는 문"으로 한정된다. 이 한정이 도커 이야기의 핵심 열쇠가 된다.지도 — 네트워크 네임스페이스는 포트 테이블을 통째로 복제한다
네트워크 네임스페이스는 네트워크 스택 전체를 격리한 별도의 방이다. 인터페이스 목록, IP 주소, 라우팅 테이블, 방화벽 규칙, 그리고 포트 사용 현황(어떤 포트가 지금 열려 있는가)까지 — 이 모든 것이 방마다 독립적으로 존재한다. 도커가 컨테이너를 띄운다는 건 이 방을 새로 하나 짓는 일이다.
그래서 "포트를 공유하느냐 새로 여느냐"라는 질문에는 이렇게 답할 수 있다. 포트 번호라는 숫자 자체는 0~65535라는 같은 범위를 쓰지만, "그 번호가 지금 누구에게 점유돼 있는가"를 기록하는 장부(포트 테이블)는 방마다 완전히 따로다. 호스트 방의 8080번과 컨테이너 방의 8080번은 이름만 같은 남남이다.
위 다이어그램은 하나의 커널 안에 네트워크 스택 세트가 방마다 통째로 복제되는 구조를 보여준다. 리눅스 커널에서 두 갈래로 갈라져 호스트 네임스페이스와 컨테이너 네임스페이스가 각자 "인터페이스·IP·라우팅·포트장부" 한 세트를 통째로 갖는다. 그 아래를 보면 8080번을 호스트에서는 호스트 nginx가, 컨테이너에서는 컨테이너 nginx가 각자 점유하고 있다 — 같은 숫자가 두 장부에 동시에 "사용 중"으로 적혀 있어도 서로 모른다. 이게 왜 자연스러운가 하면, 네임스페이스의 목적 자체가 "각 컨테이너가 마치 자기만의 독립된 기계인 것처럼 굴게 하는 것"이기 때문이다. 독립된 기계라면 당연히 자기만의 포트 장부를 가진다. nginx 컨테이너를 100개 띄워도 전부 자기 방에서 80번을 열 수 있는 이유가 바로 이것이다 — 서로 다른 100개의 포트 장부라 충돌할 일이 없다.
실험 — 호스트와 컨테이너가 같은 0.0.0.0:8099를 동시에 연다
말로만 하면 안 믿긴다. 직접 두드려 보자. 먼저 호스트에서 파이썬 서버를
0.0.0.0:8099에 띄운다. 그리고 같은 0.0.0.0:8099를 컨테이너 안에서도 연다. 만약 포트가 공유되는 자원이라면 둘째는 "주소가 이미 사용 중(Address already in use)"으로 튕겨야 한다.# 1단: 호스트 네임스페이스에서 8099를 0.0.0.0에 연다 # --bind 0.0.0.0 은 "이 방의 모든 인터페이스로 받겠다"는 뜻 python3 -m http.server 8099 --bind 0.0.0.0 & # 호스트 장부 확인 — python3가 0.0.0.0:8099를 점유 중 ss -ltnp | grep 8099 # LISTEN 0 5 0.0.0.0:8099 0.0.0.0:* users:(("python3",pid=...)) # 2단: 컨테이너를 띄워 그 안(별도 네임스페이스)에서도 8099를 연다 # 포트가 공유 자원이면 여기서 충돌로 죽어야 한다 docker run -d --name nsport alpine \ sh -c "while true; do nc -l -p 8099; done" # 결과: 죽지 않는다. 컨테이너는 멀쩡히 떠 있다 docker ps --filter name=nsport --format '{{.Status}}' # Up 1 second # 3단: 컨테이너 장부 확인 — 자기 방에서 8099가 열려 있다 docker exec nsport netstat -ltn | grep 8099 # tcp 0 0 :::8099 :::* LISTEN위 코드는 같은 포트 번호가 두 네임스페이스에서 동시에, 서로 방해 없이 열리는 것을 실제로 보여준다. 호스트에서 파이썬이
0.0.0.0:8099를 잡은 상태 그대로 컨테이너를 띄워 그 안에서 또 8099를 열었는데, 컨테이너는 "Up" 상태로 멀쩡히 살아 있다. 만약 포트가 기계 하나에 하나뿐인 공유 자원이었다면 둘째 listen은 즉시 튕겨야 했다. 그런데 각자의ss·netstat장부를 보면 호스트 장부에는 파이썬이, 컨테이너 장부에는 nc가 같은 8099를 각각 "LISTEN"으로 올려 두고 있다. 이걸로 처음 질문의 답이 나온다 — 컨테이너의 포트는 호스트와 공유하는 게 아니라 그 네임스페이스만을 위해 새로 열린다. 커널은 listen 소켓을 찾을 때 포트 번호만 보는 게 아니라 "어느 네임스페이스인가"를 함께 열쇠로 삼아 구분하기 때문에, 같은 INADDR_ANY:포트라도 방이 다르면 충돌하지 않는다.여기서 앞 절의 단서가 딱 맞아떨어진다. 0.0.0.0이 "모든 문"이라 했지만 그 "모든"은 자기 네임스페이스 안의 문으로 한정된다고 했다. 컨테이너 안에서 0.0.0.0에 bind하면 그건 컨테이너 방의 문들만 여는 것이지, 호스트 방의 문까지 여는 게 아니다. 그래서 겹쳐 보여도 실제로는 겹치지 않는다.
그렇다면 -p 8080:80은 무엇을 하는가
포트 장부가 방마다 따로라면, 컨테이너 안에서 연 80번은 호스트 바깥 세상에서 그냥은 닿을 수 없다. 바깥에서 온 패킷은 호스트 방의 포트 장부만 보기 때문이다. 이 두 방을 잇는 다리가 바로
docker run -p 8080:80의 포트 퍼블리싱(publishing)이다. 이건 "포트를 공유"하는 게 아니라 호스트 방의 8080번으로 들어온 패킷을 컨테이너 방의 80번으로 전달(forward)하도록 규칙을 하나 심는 일이다.위 다이어그램은 -p 옵션이 두 개의 독립된 포트 장부를 하나의 규칙으로 연결하는 흐름을 보여준다. 외부 클라이언트가 호스트IP의 8080번으로 접속하는 데서 출발해, 호스트 네임스페이스에서 도커가 잡아 둔 8080번을 거친다. 여기서 DNAT(Destination NAT, 목적지 주소 변환) 규칙이 패킷의 목적지를 컨테이너의 80번으로 바꿔 컨테이너 네임스페이스로 넘기고, 마지막으로 컨테이너 안 서버가 응답한다. 핵심은 8080과 80이 서로 다른 방의 서로 다른 번호라는 점이다 — 같은 포트를 공유하는 게 아니라, 방과 방 사이에 "이 번호로 오면 저 번호로 보내라"는 우편 전달 규칙이 놓인 것이다. 이 구조 덕분에 컨테이너 안 서버는 자기가 그냥 80번을 열었다고만 알면 되고, 바깥에 어떤 번호로 노출됐는지는 신경 쓸 필요가 없다.
여기서 흔히 빠지는 오해가 하나 있다.
-p 8080:80에서 호스트 쪽을-p 127.0.0.1:8080:80으로 적으면 이 전달 규칙이 호스트의 loopback 문으로 들어온 것에만 걸린다. 즉 같은 기계에서만 컨테이너에 닿고 외부 네트워크에서는 못 닿는다. 반대로 그냥-p 8080:80은 호스트의 0.0.0.0(모든 문)에 규칙을 걸어 외부에도 그대로 노출한다. 무심코 열어 둔 컨테이너가 바깥에서 다 보이는 사고가 여기서 난다 — bind 주소를 어디로 정하느냐가 노출 범위를 가르는 것은 호스트 프로세스나 컨테이너 퍼블리싱이나 똑같다.정리 — 포트는 방마다 따로, 0.0.0.0은 그 방의 모든 문
처음의 두 질문에 이제 답할 수 있다. 0.0.0.0에 listen한다는 것은 특정 랜카드 하나가 아니라 "지금 이 네임스페이스에 있는 모든 인터페이스로 오는 요청을 받겠다"는 와일드카드 선언이다. 그리고 도커가 네트워크 네임스페이스를 만들면 포트는 공유되는 게 아니라 그 방만을 위해 새로 열린다 — 포트 번호라는 숫자 범위는 같아도, 그 번호가 점유됐는지 기록하는 장부가 방마다 통째로 따로이기 때문이다. 호스트와 컨테이너가 같은 0.0.0.0:8099를 동시에 열어도 부딪히지 않는 것이 그 증거다.
이 그림을 손에 쥐면 실무에서 헷갈리던 것들이 한 줄로 풀린다. nginx 컨테이너 100개가 전부 80번을 여는 게 왜 괜찮은지(방이 100개니까), 컨테이너 안 서버에 바깥이 닿게 하려면 왜
-p가 필요한지(방과 방 사이 전달 규칙이 있어야 하니까), 그리고-p 127.0.0.1:8080:80과-p 8080:80이 왜 노출 범위가 다른지(호스트 쪽 규칙을 어느 문에 거느냐가 다르니까). 개인 서버에서 서비스를 여러 개 띄울 때, "포트가 겹치나?"를 걱정하기 전에 그 프로세스가 어느 방에서, 어느 문에 귀를 대고 있는지를 먼저 그리면 대부분의 혼란이 사라진다.
참고한 공개 자료:
- Linux kernel — network namespace 별 소켓 조회(listen lookup)에 namespace 포인터를 함께 사용해 같은 INADDR_ANY:port를 충돌 없이 허용: [patch] Network namespace ipv4 isolation (LKML)
- Docker networking과 connection refused 원인 정리: Connection refused? Docker networking and how it impacts your image (pythonspeed.com)
- 컨테이너 포트 퍼블리싱을 0.0.0.0 대신 127.0.0.1로 제한하기: Publishing docker ports to 127.0.0.1 instead of 0.0.0.0 (brokkr.net)
- 네트워크 네임스페이스 공유 시 포트 충돌 동작: Sharing Network Namespaces in Docker (mikesir87's blog)
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
OpenClaw는 Lobster의 승인자를 어떻게 지원하나 — 문에서는 신원을 인증하고, 승인자는 환경변수로 흘려보낸다 (1) 2026.09.18 Lobster에서 멈춘 워크플로우를 다시 달리게 하기 — 재개 토큰과 승인자 신원은 같은 호출에서 만난다 (0) 2026.09.17 Lobster의 approval 게이트 — 승인을 프롬프트로 부탁하는 것과 런타임이 멈추는 것은 다른 물건이다 (0) 2026.09.17 Lobster의 .lobster 파일은 LLM을 빼지 않는다 — 판단만 남기고 분기를 파서에게 넘긴다 (0) 2026.09.17 도커가 다른 네트워크의 컨테이너를 막는 법 — iptables와 FORWARD 사슬이라는 검문소 (0) 2026.09.16 PerspectiveGap — 최상위 모델 62%, 평균 17%. 오케스트레이션 프롬프팅은 코딩 실력과 다른 능력이었다 (0) 2026.09.15 호스트가 172.17.0.1로 컨테이너 요청을 받을 수 있는가 — bind 주소가 정하는 '듣는 귀' (0) 2026.09.14 docker0의 172.17.0.1은 무엇인가 — 브리지 게이트웨이는 호스트 공용 문이 아니다 (0) 2026.09.14 샌드박스 OpenClaw와 호스트 Claude Code가 같이 못 뜬 이유 — MCP 서버를 고정 포트에 올린 순간 그것은 싱글톤이 됐다 (0) 2026.09.14 파이프라인을 자동화했더니 부채가 쏟아졌다 — 갚는 비용의 단위는 항목 수가 아니라 판단 횟수다 (0) 2026.09.13