(CS 스터디) 왜 Docker는 포트포워딩이 필요할까? - Linux Network Namespace부터 iptables까지
- -
👀 들어가기 전에
어느 때처럼 평화롭게 일을 하고 있었습니다.
Docker Hub 이미지를 기반으로 컨테이너를 띄우고, 포트 매핑을 설정하고, AWS 환경 위에 서비스를 올리며 배포 구성을 마무리해가던 중이었습니다.
그런데 문득 아주 단순한 질문이 머릿속에 들어왔습니다.
왜 굳이 호스트의 8080 포트와 컨테이너의 80포트를 연결 해주는 걸까?
왜 내부 포트와 외부 포트는 분리되어 존재할까?
같은 리눅스 호스트 위에서 돌아가는 프로세스라면, 그냥 서버만 떠 있으면 접근되면 되는 것 아닌가?
오히려 지금의 구조가 더 복잡한 것은 아닐까?
물론 실무에서는 이미 익숙하게 답을 외우듯 사용합니다.
포트 충돌을 피하기 위해서, 여러 컨테이너를 동시에 띄우기 위해서, 확장성과 운영 편의 성을 위해서. 이 설명들은 틀리지 않습니다.
다만 이 설명만으로는 "왜 꼭 그런 구조여야만 하는가"에 대한 질문의 답이 되지는 못합니다.
이 질문의 답은 리눅스 커널의 네트워크 모델 안에 있습니다.
컨테이너는 단순히 프로세스 하나 처럼 보이지만, 실제로는 독립된 network namespace 안에서 실행됩니다. 컨테이너는 자신만의 네트워크 인터페이스와 라우팅 테이블, 포트 바인딩 공간을 가지며, 호스트와는 veth pair 및 bridge 네트워크를 통해 연결됩니다.
그리고 외부에서 들어온 요청이 컨테이너 내부 포트까지 도달하는 과정에는 iptables 기반 NAT와 conntrack이 개입합니다.
즉, 우리가 너무 자연스럽게 사용하던 -p 8080:80은 단순한 편의 옵션이 아닙니다.
그 한 줄 뒤에는 네트워크 격리, 주소 변환, 패킷 포워딩, 연결 상태 추적이라는 커널 레벨의 메커니즘이 숨어 있습니다.
이번 글에서는 바로 그 구조를 따라가며
왜 도커에서는 포트포워딩이 필요한가? 왜 그래야만 하는가?
라는 질문을 리눅스 네트워크 스택 관점에서 끝까지 파고들어보려 합니다.
👀 본론
1. 컨테이너는 정말 "같은 서버 위 프로세스"일 뿐일까?
Docker 컨테이너를 처음 접할 때 흔히 듣는 설명이 있습니다.
컨테이너는 리눅스 위에서 실행되는 하나의 프로세스 집합이다.
이 설명은 틀리지 않습니다. 실제로 컨테이너는 별도의 커널을 가지지 않습니다.
모든 컨테이너는 호스트의 리눅스 커널을 그대로 공유합니다.

즉, Docker는 Hypervisor 기반 가상화(VM)가 아니라 OS-level virtualization입니다.
조금 더 직설적으로 번역을 해보면,
컨테이너 = 격리된 환경에서 실행되는 리눅스 프로세스들
입니다.
그렇다면 네트워크도 그냥 공유하는 것이 아닌가?
하지만 여기서 중요한 사실이 있습니다.
"같은 커널을 공유한다"는 것과 "같은 시스템 리소스를 공유한다"는 것은 다른이야기 입니다.

리눅스 커널은 프로세스를 격리하기 위해 여러 가지 Namespace를 제공합니다.
대표적인 것들은 아래와 같습니다.

Docker는 바로 이 Namespace를 조합해서 컨테이너를 구성합니다.
그리고 우리가 지금 이해하려는 포트포워딩 문제는 network namespace와 깊게 연결되어 있습니다.
2. Network Namespace - 컨테이너는 "다른 네트워크 공간"에 존재한다?
우선, 먼저 쉬운 그림과 예시로 하나하나 설명을 드리고 공식문서 기반으로 제대로 이해가실 수 있도록 설명해보겠습니다.
리눅스의 network namespace는 프로세스에게 독립된 네트워크 스택을 제공합니다.
즉, 하나의 시스템 안에서도 서로 다른 프로세스 그룹이 완전히 별개의 네트워크 환경을 사용할 수 있습니다.

Network namespace가 분리되면 다음 요소들이 모두 독립적으로 존재하게 됩니다.
- Network Interface
- IP Address
- Routing Table
- ARP Table
- iptables rules
- Port binding space
여기서 중요한 것이 Port Binding Space입니다.
예를 들어 하나의 리눅스 시스템에서 아래 상황을 생각해보겠습니다.
[상황 1]
Process A → port 80 listen
Process B → port 80 listen
같은 네트워크 공간에서는 이 상황이 불가능합니다.
bind : address already in use
라는 에러가 발생합니다.
왜냐하면 포트는 네트워크 스택 단위로 유일해야 하기 때문입니다.
하지만 network namespace가 분리되어 있다면 이야기가 달라집니다.
Namespace A → port 80 listen
Namespace B → port 80 listen
이 경우에는 충돌이 발생하지 않습니다.
왜냐하면 두 프로세스는 서로 다른 네트워크 스택에 존재하기 때문입니다.
그리고 이것이 바로 Docker 컨테이너가 여러 개 동시에 실행될 수 있는 이유 중 하나입니다.
예를 들어 다음과 같은 상황을 생각해봅시다.

세 개의 컨테이너가 모두 같은 80 포트를 사용하고 있지만 충돌이 발생하지 않습니다.
왜냐하면 각각의 컨테이너는 자신만의 network namespace 안에서 실행되기 때문입니다.

위 Docker 공식문서의 Kernel namespaces 설명을 확인하면 위 내용을 더 자세히 이해할 수 있습니다.
(⭐️ 빨간 밑줄들을 기억해주세요!!)
Docker 공식 문서의 Kernel namespaces 설명을 보면, 컨테이너가 단순히 프로세스를 실행하는 환경이 아니라 Namespace와 Control Group을 기반으로 격리된 실행 환경이라는 것을 확인할 수 있습니다.
위 문서를 통해 몇가지 흥미로운 사실을 발견할 수 있습니다.
When you start a container with docker run, behind the scenes Docker creates a set of namespaces and control groups for the container.
즉, 우리가 실행하는 docker run 명령어는 Docker가 컨테이너를 위해 여러 종류의 namespaces(PID,NET,IPC 등)를 생성하고 그 안에서 프로세스를 실행하게 됩니다.
또한, 각 컨테이너가 독립적인 네트워크 스택을 가진다는 점도 확인할 수 있습니다.
컨테이너는 단순하게 프로세스를 격리한 것이 아니라 네트워크 환경 자체가 분리된 상태로 실행됩니다.
즉, 컨테이너 내부에는 위에서 말했던 독립적인 네트워크 스택이 존재하게 되는 것입니다.
여기서 좀 더 깊게 들어가보겠습니다.
컨테이너는 독립된 네트워크 공간인 독립된 network namespace 안에서 실행됩니다.
즉, 호스트와 컨테이너는 서로 다른 네트워크 스택을 사용합니다.
그렇다면 아래와 같은 상황에서
Client → Host:8080
Container → port 80
외부에서 들어온 요청은 어떤 과정을 통해 내부 포트까지 전달되는 것일까요?
이 질문을 답하기 위해서는 Docker 네트워크 구조에서 가장 중요한 요소인 veth pair와 bridge 네트워크를 이해해야합니다.
3. 그렇다면 Host는 Container 네트워크에 어떻게 접근할까?
앞의 Docker 공식 문서의 Kernel namespaces 설명에서 강조되었던 부분을 기억하신다면 자연스럽게 이해하실 수 있을 것이라 생각됩니다.
컨테이너는 단순히 같은 서버 위에서 실행되는 프로세스가 아니라 Docker가 생성한 namespace와 cgroup 위에서 동작하는 격리된 실행 환경입니다. 특히, 네트워크 관점에서는 각 컨테이너가 독립된 network stack을 가진다는 점이 중요합니다.

From a network architecture point of view, all containers on a given Docker host are sitting on bridge interfaces. This means that they are just like physical machines connected through a common Ethernet switch; no more, no less.
네트워크 구조 관점에서 보면, 하나의 Docker Host 위에 있는 모든 컨테이너는 bridge interface 위에 연결된 독립적인 네트워크 노드처럼 동작합니다. 컨테이너를 단순히 "같은 서버 위에서 도는 프로세스" 정도로만 이해를 하면 네트워크 동작이 설명되지 않는다는 것을 이제는 이해하실 수 있을 것이라 생각됩니다. 컨테이너는 같은 호스트 위에 존재하지만, 같은 네트워크 공간을 공유하지 않습니다.
그렇다면 다시 질문으로 돌아가
외부에서 Host:8080으로 들어온 요청은 도대체 어떻게 Container:80까지 도달할 수 있을까요?
같은 네트워크 공간이 아니라면, 분명히 어딘가에서 패킷을 전달해주는 연결 구조가 존재해야 합니다.
그리고 그 연결 구조의 핵심을 알기 위해 veth라는 것에 대해 이해해야 합니다.

Linux의 veth(4) 문서를 확인해보면, veth는 항상 쌍으로 생성되는 가상 Ethernet 장치라고 설명합니다.
즉, 다음과 같은 구조입니다.

한쪽 인터페이스로 들어온 패킷은 즉시 반대편 인터페이스로 전달됩니다.
이 지점에서 감이 빠른 분들은 이미 눈치채셨을 수도 있습니다.
Docker가 바로 이 veth pair를 활용하여 Host와 Container 네트워크를 연결합니다.
실제로 Docker Engine의 오픈소스 코드베이스인 Moby 프로젝트의 bridge driver 설계 문서를 확인해보면 아래와 같은 설명이 등장합니다.

Docker의 bridge 드라이버가 Linux bridge와 iptables를 사용해 컨테이너 네트워크 연결성을 제공하며, 기본적으로 docker0 브리지를 생성하고 각 컨테이너 endpoint와 bridge사이에 veth pair를 연결합니다.
즉 Docker의 네트워크 구조는 다음과 같은 형태를 가지게 됩니다.

아래는 Docker에서 공개한 Bridge network driver 문서입니다.

여기까지를 정리해보면, 다음과 같습니다.
컨테이너는 독립된 network namespace 안에서 실행되기 때문에 같은 포트를 사용하더라도 충돌하지 않습니다.
하지만, 그 대가로 Host와 Container는 더 이상 같은 네트워크 공간에 존재하지 않습니다.
따라서 두 네트워크 사이를 연결해주는 구조가 반드시 필요합니다.
Docker는 이 연결을 veth pair + Linux bridge (docker0) + iptables / NAT 조합을 통해 해결합니다.
그리고 이제 다시 처음 질문으로 돌아가 보겠습니다.
왜 우리는 -p 8080:80 같은 포트포워딩을 사용해야 할까요?
여기까지의 이해만으로는 다음 정도까지 추측할 수 있습니다.
Host 네트워크와 Container 네트워크 사이에는 bridge 기반 연결이 존재하며,
외부에서 들어온 요청을 Container 내부 포트로 전달하기 위해서는 추가적인 포트 매핑이 필요하다.
하지만 여전히 패킷이 실제로 어떤 경로를 따라 이동하는지는 명확하지 않습니다.
이를 정확히 이해하려면 Docker 네트워크 구조의 핵심인
- veth pair
- docker0 bridge
- iptables NAT
가 실제로 어떻게 동작하는지 살펴볼 필요가 있습니다.
4. Docker 네트워크의 실제 구조 - veth pair와 docker0 bridge
Docker 컨테이너가 생성될 때 내부적으로 어떤 네트워크 구조가 만들어지는지 실제 시스템에서 확인해보겠습니다.
Docker를 설치한 Linux 시스템에서 다음 명령어를 실행해보면

위와 같은 결과를 확인할 수 있습니다.
docker0 bridge 인터페이스와 여러 개의 veth 인터페이스를 확인할 수 있습니다.
그 중에서도 docker0는 Docker가 자동으로 생성하는 Linux bridge 인터페이스입니다.
Linux bridge는 네트워크 장비 관점에서 보면 소프트웨어 스위치와 같은 역할을 합니다.
즉, 아래와 같은 구조가 만들어집니다.

각 컨테이너가 생성될 때 Docker는 아래와 같이 작업을 수행합니다.
- 새로운 network namespace 생성
- veth pair 생성
- 한 쪽 인터페이스를 컨테이너 namespace 내부에 연결
- 다른 쪽 인터페이스를 docker0 bridge에 연결
결과적으로 컨테이너 내부에서는 다음과 같은 인터페이스가 보이게 됩니다.
그 결과 논리 구조는 아래와 같습니다.

위에서 그린 그림과 합쳐서 전체적인 구조를 보면,

컨테이너 내부에서 보이는 eth0가 실제 물리 NIC가 아니라, Host 쪽 veth와 연결된 가상 인터페이스입니다.
즉, 컨테이너 입장에서는 그냥 평범한 네트워크 카드처럼 보이지만, 실제로는 Host bridge 네트워크에 매달린 한쪽 끝입니다.
반대로 Host 쪽에서는 ip addr를 통해 docker0와 여러 개의 veth 인터페이스를 확인할 수 있습니다.
이 구조를 통해 컨테이너는 독립된 namespace를 유지하면서도 동시에 Host의 bridge 네트워크에 연결된 하나의 네트워크 노드처럼 동작하게 됩니다. 아래 Docker Desktop 문서를 통해 eth0가 docker0(가상브리지)에 연결된다고 설명한 부분을 확인할 수 있습니다.

하지만, 아직 한 가지 문제가 남아 있습니다.
외부 클라이언트는 다음 주소로 접근합니다.
Host:8080
하지만 실제 서버 컨테이너 내부에서는 다음 포트를 사용합니다.
Container:80
그렇다면 Host의 8080 포트로 들어온 패킷은 어떻게 Container의 80포트로 전달될까요?
이 질문의 답은 Docker가 생성하는 iptables NAT 규칙에 있습니다.
5. docker run -p 뒤에서 실제로 일어나는 일 (iptables NAT)
위에서 말한 문제에 대해 이야기해보겠습니다.
Host:8080 → Container:80
Host:8080으로 들어온 패킷은 자동으로 Container:80으로 전달되지 않습니다.
두 네트워크는 서로 다른 namespace에 존재하기 때문입니다.
Docker는 이 문제를 Port Publishing 기능을 통해 해결합니다.
docker run -p 8080:80 nginx
이 명령을 실행하면 Docker는 단순히 포트를 열어주는 것이 아니라, Linux iptables에 NAT 규칙을 추가합니다.
Docker는 NAT 테이블의 PREROUTING 체인에 규칙을 추가하고 DOCKER chain을 통해 DNAT을 수행합니다.
Docker가 생성한 NAT 규칙은 다음 명령어로 확인할 수 있습니다.
iptables -t nat -L -n
실제로 확인해보면 아래와 같이 규칙이 생성되어 있는 것을 볼 수 있습니다.

즉, Host의 8080 포트로 들어온 패킷의 목적지 주소와 포트를 컨테이너의 IP와 포트로 변경(DNAT) 하는 것입니다.
실제 패킷의 흐름을 정리해보면 아래와 같습니다.





컨테이너 입장에서는 이 요청이 단순히 port 80으로 들어온 일반적인 요청처럼 보이는 것입니다.
6. 왜 Docker는 포트포워딩 구조를 사용할까?
지금까지의 내용을 정리해보겠습니다.
Docker 컨테이너는 단순히 같은 서버 위에서 실행되는 프로세스가 아닙니다.
각 컨테이너는 독립된 network namespace 안에서 실행되며, 자신만의 네트워크 인터페이스와 포트 바인딩 공간을 가집니다.
이 구조 덕분에 여러 컨테이너가 동일한 포트를 사용하더라도 충돌 없이 실행될 수 있습니다.
하지만 이런 네트워크 격리는 한 가지 문제를 만듭니다.
컨테이너는 Host 네트워크와 동일한 네트워크 공간에 존재하지 않습니다.
따라서 외부에서 컨테이너 내부 포트에 직접 접근할 수 없습니다.
Docker는 이 문제를 다음 세 가지 커널 기능을 조합하여 해결합니다.
- network namespace + veth pair + linux bridge/iptables NAT
즉 우리가 평소에 아무 생각 없이 사용하던
docker run -p 8080:80
이라는 명령어는 실제로 다음과 같은 작업을 수행합니다.
- network namespace 생성
- veth pair 생성
- docker0 bridge 연결
- iptables DNAT rule 생성
- 패킷 포워딩
결국 Docker 네트워크는 Dockerrㅏ 새롭게 만든 네트워크 시스템이 아니라
Linux 커널이 제공하는 네트워크 기능들을 조합해 구현된 하나의 네트워크 가상화 계층이라고 볼 수 있습니다.
👀 마무리하며
앞에서 던졌던 질문
왜 Docker에서는 포트포워딩을 사용해야 할까?
에 대한 답을 이제는 조금 더 명확하게 할 수 있을 것 같습니다.
처음에는 단순히 의문에서 시작했습니다.
- 왜 굳이 Host의 포트와 Container의 포트를 연결해야 할까?
- 같은 서버 위에서 돌아가는 프로세스라면 그냥 접근되면 되는 것 아닐까?
- 왜 내부 포트와 외부 포트를 분리하는 구조를 사용할까?
하지만 Docker 네트워크 구조를 리눅스 커널 레벨까지 따라가보면, 이 질문의 답은 생각보다 명확해집니다.
Docker 컨테이너는 같은 서버 위에서 실행되는 프로세스가 아닙니다.
각 컨테이너는 독립된 network namespace 안에서 실행되는 격리된 환경을 가지며 이로 인해 Host와 동일한 네트워크 공간을 공유하지 않습니다. 따라서 외부에서 들어온 요청은 컨테이너 내부 포트로 직접 전달될 수 없습니다.
Docker는 이 문제를 해결하기 위해 리눅스 커널이 제공하는 여러 네트워크 기능을 조합합니다. 대표적으로 다음과 같은 요소들이 함께 동작합니다.
- network namespace
- veth pair
- Linux bridge(docker0)
- iptables NAT
이 구조를 통해 외부 트래픽은 Host 포트로 들어온 뒤 NAT 변환을 거쳐 컨테이너 내부 네트워크로 전달됩니다.
결국 우리가 평소에 자연스럽게 사용하던
docker run -p 8080:80
이라는 명령어는 단순히 포트를 열어주는 옵션이 아니라, 리눅스 네트워크 스택 위에서 동작하는 패킷 전달 메커니즘을 구성하는 과정이라고 볼 수 있습니다.
이번 글을 통해 Docker의 포트포워딩을 리눅스 네트워크 스택 관점에서 이해해보는 계기가 되었던 것 같습니다.
평소에는 아무렇지 않게 사용하던 명령어 하나에도 그 뒤에서 꽤 많은 커널 레벨의 동작들이 일어나고 있다는 점이 흥미롭게 느껴졌습니다.☺️
'Study' 카테고리의 다른 글
| (CS Study) Paging : OS Level 부터 Application Level까지 (0) | 2026.03.21 |
|---|---|
| (CS Study) Spring Bean의 생성과 DI 동작 원리 : ApplicationContext.refresh()부터 populateBean()까지 (0) | 2026.03.14 |
| (CS Study) Tomcat Internals : TCP Accept 부터 DispatcherServlet까지 (feat. Tomcat 소스코드로 추적하기) (0) | 2026.02.28 |
| (CS Study) 트랜잭션의 격리수준 어디까지 알고 계십니까? (0) | 2026.02.21 |
| (CS Study) 스레드, 어디까지 알고 계십니까? (0) | 2026.02.10 |
당신이 좋아할만한 콘텐츠
-
(CS Study) Paging : OS Level 부터 Application Level까지 2026.03.21
-
(CS Study) Spring Bean의 생성과 DI 동작 원리 : ApplicationContext.refresh()부터 populateBean()까지 2026.03.14
-
(CS Study) Tomcat Internals : TCP Accept 부터 DispatcherServlet까지 (feat. Tomcat 소스코드로 추적하기) 2026.02.28
-
(CS Study) 트랜잭션의 격리수준 어디까지 알고 계십니까? 2026.02.21
소중한 공감 감사합니다