Table of contents
- 컨테이너는 서로 어떻게 이야기하는가
- Docker 네트워크의 큰 그림
- 네트워크 드라이버 네 가지
- bridge — 가장 많이 쓰는 기본 드라이버
- user-defined bridge — 실무에서 권장되는 패턴
- host — 호스트와 네트워크를 공유한다
- none — 네트워크를 꺼버린다
- overlay — 여러 호스트에 걸친 네트워크
- 포트 포워딩 — -p 옵션의 실체
- 기본 bridge vs user-defined bridge — 한눈에 비교
- Docker Compose의 네트워크
- DNS와 서비스 디스커버리
- IPv6와 기타 옵션들
- 디버깅 — 트래픽이 안 갈 때
- 기초 파트를 지나며
컨테이너는 서로 어떻게 이야기하는가
컨테이너 하나만 띄울 때는 네트워크를 의식할 일이 없다. docker run -p 8080:80 nginx 한 줄이면 호스트의 8080으로 접속이 된다. 그런데 컨테이너가 둘, 셋이 되고, DB와 앱과 캐시가 각각 다른 컨테이너에 담기기 시작하면 갑자기 질문들이 쏟아진다.
- 앱 컨테이너는 DB 컨테이너를 어떤 주소로 부르지?
- IP가 재시작마다 바뀌면 어떻게 하나?
- 호스트에서 컨테이너의 특정 포트로 접근하려면 왜
-p가 필요한 거지? - 여러 컨테이너를 같은 네트워크에 묶는 방법은?
이 편에서는 이 모든 질문에 답한다. Docker 네트워크는 생각보다 구조가 단정한데, 구조를 잡고 나면 대부분의 네트워킹 문제가 한 장의 지도 위에서 풀린다.
Docker 네트워크의 큰 그림
먼저 한 장으로 전체 구조를 본다.
flowchart TB
subgraph HOST["Docker Host"]
subgraph BR["bridge 네트워크 (docker0)"]
C1["Container A<br/>172.17.0.2"]
C2["Container B<br/>172.17.0.3"]
end
subgraph USER["user-defined bridge (my-net)"]
C3["Container C<br/>172.18.0.2"]
C4["Container D<br/>172.18.0.3"]
end
HOSTNIC["eth0 (호스트 NIC)<br/>192.168.1.10"]
IPT["iptables NAT/FILTER"]
end
EXT["외부 네트워크"]
BR <--> IPT
USER <--> IPT
IPT <--> HOSTNIC
HOSTNIC <--> EXT
Docker는 호스트 위에 가상 네트워크 인터페이스와 리눅스 브릿지를 띄우고, 컨테이너마다 자기만의 network namespace를 만들어 가상 NIC(veth 페어)로 연결한다. iptables 규칙으로 NAT와 포트 포워딩을 건다. 겉에서 보면 마술이지만, 안은 리눅스 네트워킹의 표준 기법이다.
네트워크 드라이버 네 가지
Docker는 네트워크를 드라이버로 추상화한다. 자주 쓰는 네 가지를 먼저 보자.
| 드라이버 | 스코프 | 쓰임새 |
|---|---|---|
| bridge | 단일 호스트 | 같은 호스트의 컨테이너끼리 통신 (기본값) |
| host | 단일 호스트 | 컨테이너가 호스트의 네트워크를 그대로 사용 |
| overlay | 다중 호스트 | Swarm에서 여러 노드에 걸친 컨테이너 통신 |
| none | 단일 호스트 | 네트워크 없음 (격리 또는 커스텀 설정용) |
아래 이어 보면서 각 드라이버가 왜 이렇게 설계됐는지 파악해보자.
bridge — 가장 많이 쓰는 기본 드라이버
Docker를 설치하면 bridge, host, none 세 네트워크가 기본으로 만들어진다. docker run에서 따로 지정하지 않으면 bridge(기본 이름: bridge)가 쓰인다.
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# 8f1e2d3c4b5a bridge bridge local
# 7c2d3e4f5a6b host host local
# 6b3c4d5e6f7a none null local
기본 bridge에 속한 컨테이너들은 서로 IP로는 통신할 수 있다. 그런데 IP는 재시작할 때마다 바뀔 수 있다. 기본 bridge에는 DNS 기반 이름 해석이 없다. 이게 기본 bridge의 결정적 약점이다.
# 기본 bridge에 띄우기
docker run -d --name db postgres:16
docker run -it --rm --link db alpine sh
# ping db (--link 덕분에 /etc/hosts에 등록돼 가능)
--link 옵션은 레거시다. 요즘은 user-defined bridge를 쓰고, 컨테이너 이름으로 바로 통신한다.
user-defined bridge — 실무에서 권장되는 패턴
내가 직접 만든 bridge 네트워크다. Docker가 자동 DNS를 제공해서, 같은 네트워크의 컨테이너끼리 컨테이너 이름으로 서로를 찾을 수 있다.
# 새 네트워크 만들기
docker network create app-net
# 두 컨테이너를 같은 네트워크에 띄우기
docker run -d --name db --network app-net \
-e POSTGRES_PASSWORD=secret \
postgres:16
docker run -d --name api --network app-net \
-e DB_HOST=db \
-e DB_USER=postgres \
-e DB_PASSWORD=secret \
myapp:1.0
핵심은 -e DB_HOST=db. API 컨테이너 안에서 db라는 호스트명으로 DB에 접속할 수 있다. Docker의 내장 DNS가 컨테이너 이름을 현재 IP로 해석해준다. DB가 재시작돼서 IP가 바뀌어도 이름은 그대로라 앱 코드를 안 고쳐도 된다.
user-defined bridge의 이점은 여러 가지다.
- 자동 DNS: 컨테이너 이름이 곧 호스트명
- 더 나은 격리: 이 네트워크 안의 컨테이너끼리만 서로 본다. 다른 네트워크의 컨테이너와는 기본 차단
- 런타임 분리/합류: 컨테이너를 네트워크에 붙였다 뗄 수 있음
# 컨테이너를 네트워크에 추가/제거
docker network connect app-net cache
docker network disconnect app-net cache
실무에서 여러 컨테이너를 묶을 땐 기본 bridge 말고 user-defined bridge를 쓰는 게 표준이다. Docker Compose는 프로젝트마다 자동으로 user-defined bridge를 만들어주기 때문에 Compose를 쓰면 이 문제를 자연스럽게 피해간다.
host — 호스트와 네트워크를 공유한다
--network host는 컨테이너가 network namespace를 따로 안 갖고 호스트와 네트워크를 공유하게 만든다. 컨테이너 안의 eth0이 호스트의 eth0 그대로다.
docker run --rm --network host nginx:1.27
# 호스트 80 포트를 바로 점유
장점은 속도다. NAT를 거치지 않으니 네트워크 I/O 오버헤드가 없다. UDP 스트리밍, 고성능 프록시, 레이턴시에 민감한 서비스에서 유용하다.
단점도 크다.
- 포트 충돌: 호스트 80 포트가 이미 쓰이면 컨테이너가 뜨지 못함
- 격리 상실: 컨테이너가 호스트의 모든 네트워크 인터페이스를 본다. 보안상 불리
- 플랫폼 제약: Linux 호스트에서만 온전히 동작. Docker Desktop(macOS/Windows)에서는 한계가 있음
특별한 이유가 없다면 host보다 bridge + -p가 낫다. 성능이 정말 절실하거나 호스트 네트워크에 직접 바인딩해야 할 때만 쓴다.
none — 네트워크를 꺼버린다
--network none은 컨테이너에 네트워크를 아예 달지 않는다. loopback(lo)만 있고, 외부 통신 불가.
docker run --rm --network none alpine ip addr
# 1: lo: <LOOPBACK> 만 보임
쓰임새는 협소하지만 두 가지 상황에서 유용하다.
- 강한 격리가 필요한 배치 작업 (외부 통신이 필요 없는 계산)
- 커스텀 네트워크를 직접 구성하려는 특수 시나리오 (CNI 플러그인 등)
일반 개발에선 거의 쓰지 않는다. “이런 것도 있다”만 알고 넘어가자.
overlay — 여러 호스트에 걸친 네트워크
overlay 드라이버는 여러 Docker 호스트에 걸친 컨테이너들이 마치 같은 네트워크 안에 있는 것처럼 통신하게 해준다. Docker Swarm에서 주로 쓰인다.
flowchart LR
subgraph H1["Docker Host 1"]
C1["Container A"]
VX1["VXLAN"]
end
subgraph H2["Docker Host 2"]
C2["Container B"]
VX2["VXLAN"]
end
C1 --> VX1
VX1 <-->|UDP 4789<br/>VXLAN 터널| VX2
VX2 --> C2
내부적으로 VXLAN을 써서 호스트 간에 L2 터널을 만든다. 컨테이너 A와 B는 서로 다른 호스트에 있어도 같은 서브넷에 있는 것처럼 보인다.
Swarm을 안 쓴다면 overlay를 직접 만질 일은 드물다. Kubernetes는 Docker overlay 대신 CNI 플러그인(Flannel, Calico, Cilium 등)을 통해 비슷한 일을 더 유연하게 처리한다. 이 시리즈에서는 “이런 드라이버가 있고, Swarm/CNI가 뒤를 잇는다”는 맥락만 잡고 넘어간다.
포트 포워딩 — -p 옵션의 실체
-p 8080:80은 무슨 일을 하는가? 호스트 8080 포트로 오는 트래픽을 컨테이너의 80 포트로 전달한다. Docker는 이걸 iptables NAT 규칙으로 구현한다.
docker run -d --name web -p 8080:80 nginx:1.27
# 호스트에서 실제 iptables NAT 테이블을 들여다보면 (리눅스 호스트)
sudo iptables -t nat -L DOCKER -n
# Chain DOCKER (2 references)
# target prot opt source destination
# DNAT tcp -- anywhere anywhere tcp dpt:8080 to:172.17.0.2:80
DNAT 규칙 한 줄이 트래픽을 돌린다. 정확히는 다음이 일어난다.
- 외부에서 호스트
0.0.0.0:8080으로 TCP 요청 도착 - iptables가 패킷의 목적지를 컨테이너 IP의 80 포트로 바꿈 (Destination NAT)
- bridge 네트워크를 통해 컨테이너 안으로 전달
- 응답 경로에선 반대로 SNAT이 적용됨
-p 옵션은 포맷이 몇 가지다.
# 호스트 포트 : 컨테이너 포트
-p 8080:80
# 인터페이스 고정 — 외부에서 접근 불가, 로컬에서만 접근 가능
-p 127.0.0.1:8080:80
# 호스트 포트 랜덤 지정 — Docker가 비어 있는 포트를 고름
-p 80
# UDP 포트
-p 53:53/udp
# 여러 개
-p 8080:80 -p 8443:443
127.0.0.1로 바인딩하는 건 보안상 중요하다. 아무 설정 없이 -p 5432:5432를 쓰면 DB가 전 세계에 열린다. 로컬에서만 쓸 거라면 -p 127.0.0.1:5432:5432로 범위를 좁힌다.
기본 bridge vs user-defined bridge — 한눈에 비교
두 bridge의 차이를 정리한다. 실무 선택에 바로 쓸 수 있다.
| 항목 | 기본 bridge | user-defined bridge |
|---|---|---|
| 생성 | Docker 설치 시 자동 | docker network create로 명시 |
| DNS | 없음 (IP로만 통신) | 있음 (컨테이너 이름 해석) |
| 격리 | 모든 컨테이너가 같은 네트워크 | 네트워크별로 분리 |
| 연결/해제 | 재생성 필요 | 런타임 connect/disconnect |
--link | 지원 (레거시) | 불필요 |
| 권장 여부 | 레거시/테스트용 | 실무 권장 |
Docker Compose의 네트워크
Compose는 프로젝트마다 자동으로 user-defined bridge를 만든다. 다음 docker-compose.yml을 보자.
services:
api:
image: myapp:1.0
ports:
- "8080:3000"
environment:
DB_HOST: db
REDIS_HOST: cache
depends_on:
- db
- cache
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7
volumes:
pgdata:
docker compose up -d를 치면 Compose는 이렇게 한다.
<프로젝트명>_default라는 user-defined bridge를 만든다- 세 서비스를 이 네트워크에 모두 붙인다
- 각 서비스명이 그대로 DNS 호스트명이 된다 (
db,cache,api) api:8080만 호스트 포트에 포워딩한다. DB와 Redis는 외부 노출 없음
결과적으로 API는 DB_HOST=db, REDIS_HOST=cache로 내부 통신을 하고, 외부에서는 오직 API의 8080만 접근 가능한 깔끔한 구성이 된다. 이게 실무에서 가장 자주 마주치는 네트워크 패턴이다.
여러 네트워크를 나누고 싶다면 Compose에서 명시할 수 있다.
services:
api:
networks: [frontend, backend]
db:
networks: [backend]
nginx:
networks: [frontend]
networks:
frontend:
backend:
internal: true # 외부 접근 차단 네트워크
internal: true인 backend 네트워크는 호스트로부터 고립된다. DB 같은 민감 서비스를 이 안에 두고, 외부 접점(nginx)은 frontend에만 두는 식으로 단계별 보안이 가능하다.
DNS와 서비스 디스커버리
user-defined bridge의 DNS는 기본적으로 컨테이너 이름을 해석한다. --network-alias로 별칭도 줄 수 있다.
docker run -d --name primary-db --network app-net \
--network-alias db \
postgres:16
# "db"라는 이름으로도 접근 가능
여러 컨테이너에 같은 alias를 주면 라운드로빈 DNS처럼 동작한다.
docker run -d --name api1 --network app-net --network-alias api nginx
docker run -d --name api2 --network app-net --network-alias api nginx
# 다른 컨테이너에서 "api"로 질의하면 두 IP가 번갈아 반환됨
단, 이건 DNS 단의 분산일 뿐 로드밸런싱은 아니다. 진짜 로드밸런싱이 필요하면 앞에 nginx/haproxy를 두거나 Swarm/Kubernetes의 Service를 쓴다.
IPv6와 기타 옵션들
네트워크를 만들 때 서브넷, 게이트웨이, 드라이버 옵션을 명시할 수 있다.
docker network create \
--driver bridge \
--subnet 10.20.0.0/16 \
--gateway 10.20.0.1 \
--ip-range 10.20.10.0/24 \
custom-net
IPv6도 활성화할 수 있다.
docker network create --ipv6 --subnet 2001:db8::/64 v6-net
대부분의 프로젝트에선 Docker가 잡아주는 기본 서브넷으로 충분하다. 서브넷 충돌(회사 내부망과 Docker 기본 네트워크가 같은 대역이면 라우팅 혼란)이 날 때만 조정한다.
디버깅 — 트래픽이 안 갈 때
실전에서 가장 자주 쓰는 네트워크 디버깅 절차를 정리한다.
# 컨테이너의 네트워크 정보
docker inspect --format '{{json .NetworkSettings.Networks}}' web | jq
# 네트워크에 속한 컨테이너 목록
docker network inspect app-net --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}\n{{end}}'
# 컨테이너 안에서 DNS 확인
docker exec -it api nslookup db
docker exec -it api getent hosts db
# 포트 리스닝 확인
docker exec -it api netstat -tlnp
docker exec -it api ss -tlnp # 요즘 권장
# 호스트에서 포트 포워딩 확인
ss -tlnp | grep 8080
sudo iptables -t nat -L DOCKER -n --line-numbers
ping으로 이름 해석은 되는데 TCP 연결이 안 되면 컨테이너 안에서 해당 포트를 안 열어둔 거다. DNS 자체가 안 되면 네트워크 소속이나 drive 종류를 의심한다. 기본 bridge에서 이름 해석이 안 되는 건 “버그가 아니라 정상 동작”이다.
기초 파트를 지나며
1편에서 컨테이너와 VM의 차이, 2편에서 이미지와 레이어, 3편에서 Dockerfile, 4편에서 생명주기, 5편에서 데이터를 지나 네트워크까지 왔다. 이 여섯 개가 Docker의 핵심 개념이다. 한 문장으로 요약하면 이렇다. Docker는 리눅스 커널 기능으로 만든 격리된 프로세스를, 이미지라는 패키지 형태로 재현 가능하게 실행하고, 볼륨과 네트워크로 외부 세계와 연결해주는 도구다.
7편부터는 본격적인 실전 운영 주제로 들어간다. 여러 컨테이너를 함께 다루는 Docker Compose, 이미지 최적화와 레지스트리, 보안, BuildKit, 프로덕션 모범 사례로 이어진다.
다음 편에서는 Docker Compose로 여러 컨테이너를 묶어 운영하는 방법을 다룬다.




Loading comments...