[2026] Docker 네트워킹 심화 가이드 — CNI·veth·브리지·iptables·서비스 디스커버리
이 글의 핵심
컨테이너 네트워크는 “가상 NIC 한 쌍(veth) + 브리지 + NAT”의 조합으로 설명할 수 있지만, 오케스트레이션 계층에서는 CNI(Kubernetes)와 libnetwork(Docker 엔진)가 각각 다른 계약으로 이 조합을 구성합니다. 이 글에서는 Linux 커널 위의 데이터 경로와 제어 경로, 포트 매핑 규칙, 이름 해석·디스커버리, 프로덕션에서 자주 쓰는 패턴을 내부 동작 중심으로 정리합니다.
이 글의 핵심
Docker 네트워킹을 “-p 8080:80이면 된다” 수준에서 한 단계 넘기면, 패킷이 어디로 가고, 주소 변환이 어디서 일어나며, 이름은 누가 해석하는지를 추적할 수 있습니다. 장애 시 증상(연결 거부, 타임아웃, DNS 실패)이 각각 다른 계층을 가리키기 때문에, 데이터 플레인(커널·iptables/nft)과 컨트롤 플레인(런타임·CNI·Docker DNS)을 구분해 보는 것이 중요합니다.
이 글에서 다루는 범위는 다음과 같습니다.
- CNI(Container Network Interface) — 스펙과 플러그인 체인, Kubernetes와의 관계, Docker 엔진(libnetwork)과의 대응
- veth 쌍과 Linux bridge — 컨테이너·호스트 간 가상 이더넷,
docker0또는 사용자 정의 브리지 토폴로지 - iptables(및 nftables) 포트 매핑 — DNAT/SNAT, MASQUERADE, Docker가 설치하는 체인 개념
- 서비스 디스커버리 — Docker 임베디드 DNS, Swarm, Kubernetes DNS, 외부 레지스트리(Consul 등)와의 연계
- 프로덕션 패턴 — 사용자 정의 브리지,
host/none/MACVLAN·오버레이, 보안·관측성 관점의 선택 기준
1. CNI(Container Network Interface)와 컨테이너 런타임 네트워크
1.1 CNI가 해결하는 문제
CNI는 컨테이너의 네트워크 네임스페이스에 IP 라우팅·인터페이스를 붙이는 작업을 표준화한 플러그인 규약입니다. kubelet(또는 동등한 오케스트레이터)은 Pod가 생성될 때 CNI ADD 호출로 인터페이스·주소·라우트를 구성하고, 삭제 시 DEL로 정리합니다. 스펙상으로는 VERSION, CHECK 등도 정의되어 있으며, 플러그인은 실행 파일으로 배치되고 JSON 설정을 stdin으로 받습니다.
대표적인 CNI 플러그인 유형은 다음과 같습니다.
| 유형 | 역할 예시 |
|---|---|
| 메인(Main) | Pod에 eth0 할당, 노드·클러스터 라우팅과 연동 (Calico, Cilium, AWS VPC CNI 등) |
| IPAM | IP 풀·할당 (host-local, 특정 메인 플러그인 내장 IPAM) |
| 메타/체인 | 여러 플러그인을 순서대로 실행 (chaining) |
| 보조 | bandwidth 제한, portmap 호스트 포트 매핑 등 |
프로덕션에서는 단일 바이너리가 모든 것을 하는 경우도 있고, CNI 설정 JSON에서 plugins 배열로 체인을 구성하는 경우도 흔합니다.
1.2 Docker 엔진은 “CNI”가 아니라 libnetwork(CNM)
중요한 구분: Docker 데몬이 컨테이너를 띄울 때 사용하는 기본 네트워크 구현은 libnetwork 기반이며, 개념적으로는 CNM(Container Network Model) 에 가깝습니다. 네트워크 드라이버(bridge, overlay, macvlan 등)가 엔드포인트를 만들고, containerd/runc가 만든 네트워크 네임스페이스에 연결합니다.
따라서 문서에서 “Docker는 CNI를 쓴다”고 단정하면 부정확합니다. 정확히는 다음과 같습니다.
- Kubernetes 워커 노드에서 Pod 네트워크: CNI 플러그인이 담당하는 경우가 일반적입니다.
- docker run / Compose로 띄운 컨테이너: libnetwork 드라이버가 담당합니다.
- 공통점: 둘 다 결국 Linux 네트워크 네임스페이스, veth, bridge, 라우팅/NAT 같은 커널 기능을 조합합니다.
실무 함의: 동일한 호스트에서 Docker 네트워크와 Kubernetes CNI를 섞어 디버깅할 때, 규칙이 iptables/nft의 어느 체인에 쌓였는지, 인터페이스 이름이 무엇인지를 구분해 봐야 합니다.
1.3 containerd + CNI (Kubernetes 관점)
노드에서 containerd가 CRI로 동작할 때, CNI 바이너리는 보통 /opt/cni/bin 등에 두고, kubelet이 Pod 샌드박스 네트워크 네임스페이스에 대해 CNI를 호출합니다. 네트워크 정책(NetworkPolicy)은 플러그인 구현에 따라 iptables/nft/eBPF로 강제됩니다.
(개념도) kubelet → CNI ADD → [main plugin] → veth + IP + routes
→ [optional] portmap, bandwidth ...
Docker 전용 글이라도 CNI를 이해하면 Kubernetes 네트워크 트러블슈팅으로 바로 연결되므로, 운영자에게는 “커널은 같고, 설정 계약만 다르다”고 기억하는 것이 유익합니다.
2. veth 쌍과 브리지 아키텍처
2.1 veth(virtual Ethernet)란
veth는 항상 쌍으로 존재하는 가상 이더넷 장치입니다. 한쪽에서 들어온 프레임은 다른 쪽으로 전달됩니다. 컨테이너 네트워킹에서 흔한 패턴은 다음과 같습니다.
- 한쪽 veth: 컨테이너의 네트워크 네임스페이스 안에서
eth0등으로 보임 - 다른 쪽 veth: 호스트 네임스페이스에 있으며, Linux bridge(예:
docker0또는br-xxxxxxxx)에 슬레이브로 붙음
이렇게 하면 같은 브리지에 연결된 컨테이너들은 L2에서 서로 통신할 수 있고, 브리지는 스위치와 유사한 동작을 합니다.
2.2 Linux bridge와 docker0
기본 bridge 네트워크를 쓰면 Docker는 보통 브리지 인터페이스(과거 기본 이름 docker0, 사용자 정의 네트워크는 br-해시)를 만들고, 컨테이너마다 veth 쌍을 생성해 연결합니다. 브리지 IP는 호스트 쪽 게이트웨이 역할을 하며, 서브넷은 Docker IPAM 설정에 따릅니다.
flowchart LR
subgraph host[호스트 네임스페이스]
BR[Linux bridge / docker0·br-xxx]
VETHh[veth 호스트 끝]
BR --- VETHh
end
subgraph ctn[컨테이너 네트워크 네임스페이스]
ETH[eth0]
end
VETHh ---|veth pair| ETH
2.3 왜 veth 이름이 @if로 보이나
ip link 출력에서 vethXXXX@ifN 형태로 보이는 것은 피어 인덱스를 나타내는 표기입니다. 어느 네임스페이스의 인덱스와 연결되었는지 추적할 때 유용하며, 컨테이너 내부 eth0과 호스트 쪽 veth를 짝지을 때 자주 확인합니다.
# 브리지와 연결된 인터페이스 확인 (호스트에서)
ip link show master docker0
# 또는 사용자 정의 브리지
bridge link
2.4 사용자 정의 bridge 네트워크가 권장되는 이유
기본 bridge와 달리 docker network create로 만든 bridge는 컨테이너 이름 기반 DNS, 격리된 서브넷, 설정 가능한 옵션 등을 일관되게 제공합니다. Compose에서 기본 네트워크를 명시적으로 두는 패턴도 이와 맞닿아 있습니다.
3. iptables(및 nftables)와 포트 매핑
3.1 -p 8080:80이 의미하는 것
호스트의 TCP 8080으로 들어온 트래픽을 컨테이너 IP의 TCP 80으로 넘기려면, 일반적으로 주소 변환(NAT) 이 필요합니다. 컨테이너 IP는 사설 대역인 경우가 많고, 외부에서는 호스트 IP:포트로만 접근하기 때문입니다.
개념적 흐름은 다음과 같습니다.
- DNAT(Destination NAT): 목적지
호스트IP:8080→컨테이너IP:80 - 필터링/연결 추적:
conntrack이 상태를 추적해 응답 패킷을 올바르게 되돌림 - SNAT/MASQUERADE(출발지 변환): 컨테이너가 외부로 나갈 때 호스트 IP로 보이게 하는 경우가 많음(마스커레이드)
3.2 Docker가 설치하는 NAT 규칙(개념)
실제 체인 이름·우선순위는 Docker 버전, iptables vs nftables 백엔드, rootless 여부에 따라 다릅니다. 다만 사고 분석에서 찾는 것은 대체로 동일합니다.
nat테이블:PREROUTING에서 DNAT로 호스트 포트를 컨테이너로 연결filter테이블: 포워딩 허용(FORWARD) 규칙- Docker용 사용자 정의 체인:
DOCKER등으로 분리되어 있음
# (참고) nat 테이블의 DOCKER 관련 규칙 확인 — 환경에 따라 출력이 다를 수 있음
sudo iptables -t nat -S | sed -n '1,120p'
sudo iptables -t nat -L DOCKER -n -v 2>/dev/null
최신 환경에서는 nftables가 백엔드이고, iptables-nft 호환 레이어를 통해 비슷한 규칙이 보일 수 있습니다. 증상이 “규칙은 있는데 연결이 안 된다”일 때는 다른 방화벽 프론트엔드(firewalld, ufw, 클라우드 SG)가 추가 DROP을 걸고 있는지 확인합니다.
3.3 포트 매핑이 실패하는 대표 원인
- 호스트 프로세스가 이미 해당 포트 사용:
Address already in use - 바인딩 주소:
-p 127.0.0.1:8080:80은 로컬호스트에서만 접근 가능 - IPv4/IPv6 혼동: 클라이언트가
localhost를::1로 해석하는 경우 등 - 루트리스 Docker / 사용자 네트워크: 포트 프록시 메커니즘이 달라 동일한 iptables 패턴이 아닐 수 있음
3.4 host 네트워크 모드와 iptables
--network host는(리눅스에서) 컨테이너가 호스트 네트워크 스택을 공유하므로 별도 DNAT 없이 포트가 열립니다. 대신 격리가 약해지고 포트 충돌이 직접 발생합니다. 성능·지연 최소화가 필요할 때 선택하지만, 멀티 테넌트·보안 관점에서는 신중해야 합니다.
4. 서비스 디스커버리 메커니즘
4.1 Docker 임베디드 DNS
사용자 정의 bridge 네트워크에 붙은 컨테이너는 일반적으로 컨테이너 이름·네트워크 별칭(alias) 을 내장 DNS(127.0.0.11 등) 로 해석합니다. 이는 외부 DNS가 아니라 Docker 데몬이 제공하는 이름 해석 경로이며, 같은 네트워크에 있는 서비스를 안정적으로 찾게 합니다.
# docker-compose 예: 서비스 이름이 DNS 이름으로 사용됨 (동일 네트워크)
services:
api:
image: my-api:latest
web:
image: nginx:alpine
depends_on: [api]
# api 컨테이너로 프록시할 때 http://api:8080 처럼 이름 접근 가능(네트워크·드라이버 전제)
주의: 기본 bridge(legacy) 는 링크(--link) 에 의존하는 오래된 패턴이 있고, 현재는 사용자 정의 네트워크를 쓰는 것이 권장됩니다.
4.2 Docker Swarm 모드
Swarm에서는 라우팅 메시, VIP, DNSRR 등 서비스 단위 디스커버리가 추가됩니다. 서비스 이름이 여러 태스크(복제본) 뒤로 로드밸런싱되는 방식은 운영 난이도와 디버깅 포인트(세션 스티키 여부, 연결 드레이닝 등)가 달라집니다.
4.3 Kubernetes의 DNS
Kubernetes에서는 일반적으로 CoreDNS가 클러스터 내부 서비스 DNS(Service 이름, 네임스페이스 한정 이름)를 제공합니다. CNI가 만든 Pod IP와 kube-proxy(또는 eBPF 기반 대체 구현)가 만든 서비스 VIP/로드밸런싱이 합쳐져 “이름 → VIP → 백엔드 Pod” 흐름이 됩니다.
4.4 외부 서비스 메시·레지스트리
Consul, etcd, Eureka(레거시), 클라우드 로드밸런서 + 타겟 그룹 등은 컨테이너 외부에서 등록·헬스체크·조회를 담당합니다. Docker만으로는 부족한 글로벌 디스커버리가 필요하면 사이드카, Ingress, 서비스 메시(Istio, Linkerd 등)와 함께 설계합니다.
5. 프로덕션 Docker 네트워킹 패턴
5.1 사용자 정의 bridge + 명시적 서브넷
팀·환경별로 브리지 네트워크를 나누고, 서브넷·게이트웨이를 고정하면 IP 충돌 분석, 방화벽 규칙, 모니터링 대시보드가 단순해집니다. Compose에서는 networks: 블록으로 선언적으로 고정합니다.
5.2 overlay 멀티 호스트 (Swarm)
여러 도커 호스트에 걸친 서비스는 overlay 네트워크(VXLAN 등)로 L2/L3 오버레이를 구성합니다. 암호화·키 로테이션·MTU 이슈가 운영 이슈로 직결되므로, 장기적으로는 Kubernetes + CNI로 이전하는 사례도 많습니다.
5.3 MACVLAN / IPVLAN
컨테이너에 물리 네트워크와 유사한 L2 주소를 직접 붙여야 할 때 macvlan을 검토합니다. 스위치·라우터 설정, 프로미스큐어스 모드, DHCP 충돌 등 네트워크 팀과의 협업이 필수입니다.
5.4 역방향 프록시와 TLS 종료
프로덕션에서는 Nginx, Traefik, Envoy 등으로 TLS 종료, 경로 기반 라우팅, 레이트 리밋을 두는 패턴이 흔합니다. 컨테이너 포트 노출을 최소화하고, 프록시 뒤 서비스만 내부 네트워크에 두면 공격 표면이 줄어듭니다.
5.5 관측성: 어디를 캡처할까
- 호스트:
bridge fdb,ip -d link,conntrack -L(환경에 따라),ss - 컨테이너:
ip route,getent hosts, 애플리케이션 로그 - 패킷 추적:
tcpdump -i any port 8080(권한·민감 정보 주의)
5.6 보안 체크리스트(요약)
- 불필요한 포트 publish 금지:
0.0.0.0바인딩 최소화 - 신뢰 경계: 퍼블릭 서비스와 데이터베이스를 다른 네트워크로 분리
- 최신 Docker/커널: CVE 대응, nft/iptables 혼재 이슈 추적
- Kubernetes로 갈 경우: NetworkPolicy로 기본 거부·최소 허용 설계
정리
Docker 네트워킹의 내부는 veth + bridge + NAT로 요약할 수 있지만, 운영 환경에서는 libnetwork vs CNI, iptables vs nft, 내장 DNS vs Kubernetes DNS처럼 제어 경로가 달라집니다. 동일한 증상이라도 DNAT 누락, DNS NXDOMAIN, FORWARD DROP, MTU/단편화 등 원인이 나뉘므로, 계층별로 가설을 세우고 증거(ip, iptables/nft, 애플리케이션 로그)로 검증하는 습관이 가장 큰 생산성 향상으로 이어집니다.
참고로 살펴볼 명령 (호스트)
# 브리지·veth·마스터 관계
ip -br link
bridge link show
# 라우팅·ARP
ip route
ip neigh show
# NAT 규칙(환경에 따라 nft로 대체)
sudo iptables -t nat -L -n -v
위 명령은 프로덕션에서 읽기 전용으로 상태를 스냅샷할 때 유용하며, 규칙을 직접 편집하면 Docker 데몬과 충돌할 수 있으므로 인프라 코드(IaC)와 데몬 설정으로 관리하는 편이 안전합니다.
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「[2026] Docker 네트워킹 심화 가이드 — CNI·veth·브리지·iptables·서비스 디스커버리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「[2026] Docker 네트워킹 심화 가이드 — CNI·veth·브리지·iptables·서비스 디스커버리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.