웹 컨테이너에서 curl http://db:5432를 실행했는데 이름을 찾지 못한다는 에러가 났습니다. 두 컨테이너는 같은 서버에 있었지만 기본 bridge 네트워크에서는 컨테이너 이름을 DNS로 해석해주지 않았습니다.
IP를 직접 넣어 임시로 해결했더니 재시작 후 IP가 바뀌어 다시 깨졌습니다. 컨테이너 간 통신은 네트워크를 어떻게 만들고 연결하느냐에 따라 안정성이 완전히 달라집니다.
컨테이너 간 통신과 Docker 네트워크
컨테이너는 기본적으로 서로 격리된 환경에서 동작합니다. 웹 서버가 데이터베이스에 접근하고, 마이크로서비스들이 서로 API를 호출하려면 네트워크 설정이 필요합니다. Docker 네트워크를 이해하면 컨테이너 간 통신을 안전하고 효율적으로 구성할 수 있습니다.
컨테이너들이 어떻게 서로를 찾고 통신하는지, 네트워크를 어떻게 격리해야 보안이 강화되는지 직접 실습으로 이해합니다.
- 1bridge / host / none / overlay 4종 네트워크 드라이버를 비교해 선택할 수 있다
- 2기본 bridge 네트워크의 한계와 사용자 정의 bridge 네트워크의 장점을 설명할 수 있다
- 3docker network create로 격리된 네트워크를 구성하고 컨테이너를 연결할 수 있다
- 4사용자 정의 네트워크에서 컨테이너 이름이 DNS로 해석되는 원리를 설명할 수 있다
- 5네트워크 격리 전략으로 컨테이너 간 통신 허용/차단을 설계할 수 있다
컨테이너 간 통신 실습을 위해 curl 도구가 필요합니다. 호스트에 없더라도 curlimages/curl 컨테이너로 대체할 수 있습니다.
docker pswhich curl || sudo apt-get install -y curldocker network lsdocker pull nginx:alpine && docker pull curlimages/curlDocker 네트워크의 세 가지 기본 드라이버
웹 서버 컨테이너에서 curl http://db:5432를 실행했는데 연결이 안 됩니다. 같은 호스트에서 두 컨테이너를 실행했는데도 서로 찾지 못하는 상황입니다. 컨테이너는 기본적으로 격리된 네트워크 네임스페이스를 가지기 때문에, 어떤 네트워크 드라이버를 통해 연결하느냐에 따라 통신 방식이 완전히 달라집니다. 기본 브릿지(docker0)는 IP 주소로만 통신 가능하고, 사용자 정의 네트워크는 이름으로 통신 가능합니다. Host 모드는 격리를 포기하고 최고 성능을 택하며, None은 네트워크를 완전히 차단합니다. 이 ConceptBlock에서는 Docker가 제공하는 세 가지 기본 네트워크 드라이버와 각각의 동작 방식을 다룹니다.
Docker는 컨테이너 간 통신 방식에 따라 세 가지 핵심 네트워크 드라이버를 제공합니다.
확대
브릿지(Bridge) 네트워크
브릿지 네트워크는 Docker의 기본 네트워크 모드입니다. Docker를 설치하면 docker0라는 가상 브릿지 인터페이스가 자동 생성되고, 별도 설정 없이 실행한 모든 컨테이너는 이 브릿지에 연결됩니다. 호스트의 물리 네트워크 위에 Docker가 만든 가상 스위치로 이해하면 됩니다. 아래 구조를 보면 컨테이너들이 호스트와 분리된 사설 IP 대역에서 동작한다는 것이 명확해집니다.
호스트 머신의 인터페이스 구성은 다음과 같습니다.
eth0— 물리 네트워크 (192.168.1.100)docker0— 가상 브릿지 (172.17.0.1)- container-A (172.17.0.2)
- container-B (172.17.0.3)
컨테이너들은 172.17.0.x 대역의 사설 IP를 받아서 서로 통신할 수 있습니다. 단, 기본 브릿지(docker0)에는 내장 DNS가 없습니다. 이것이 핵심 제약사항입니다.
Host 네트워크
--network host 옵션을 사용하면 컨테이너가 호스트의 네트워크 네임스페이스를 그대로 사용합니다. 가상 네트워크 인터페이스가 없으므로 포트 매핑(-p)도 불필요합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part3/exam_10 && cd /tmp/docker/part3/exam_10
# Host 모드로 nginx 실행 — 호스트의 80포트를 직접 사용
docker run --network host nginx
장점: NAT 오버헤드 없이 최고의 네트워크 성능 단점: 포트 충돌 위험, 컨테이너 격리 약화, macOS/Windows에서 미지원
None 네트워크
--network none은 루프백(127.0.0.1) 외 모든 네트워크 인터페이스를 비활성화합니다. 완전한 네트워크 격리가 필요한 보안 작업이나 배치 처리에 사용합니다.
# 네트워크 없는 완전 격리 컨테이너
docker run --network none alpine sh -c "echo '외부 접근 불가'"
현재 네트워크 목록 확인
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# abc123def456 bridge bridge local ← 기본 docker0
# xyz789uvw012 host host local
# 111aaa222bbb none null local
미사용 Docker 네트워크 삭제
안전한 실행 조건: 삭제 대상이 실습 또는 임시 네트워크뿐임을 확인한 경우에만 실행합니다.
실행 전 반드시 확인
- docker network ls로 네트워크 목록을 확인했다
- 운영 Compose/Swarm 네트워크가 아님을 확인했다
- 연결된 컨테이너가 없는 네트워크만 삭제 대상임을 이해했다
docker network prune위 항목을 모두 확인한 후 복사할 수 있습니다
컨테이너가 네트워크에 연결되는 법 — veth·bridge·iptables
docker run 한 줄로 컨테이너가 네트워크에 붙는 5단계 — netns·veth·bridge·NAT
docker run -p 8080:80 nginx 한 줄이면 컨테이너가 자기 IP를 받고, 밖으로 인터넷도 나가고, 호스트 8080으로 들어온 요청도 받습니다. 이 짧은 순간에 리눅스 커널에서는 network namespace 생성 → veth 쌍 연결 → IP 할당 → 나갈 때 SNAT → 들어올 때 DNAT가 순서대로 엮입니다. 이 흐름을 알면 "컨테이너가 왜 IP를 못 받지", "왜 컨테이너에서 인터넷이 안 되지", "왜 -p 를 줬는데 접속이 안 되지"를 단계로 좁혀 진단할 수 있습니다. 컨테이너 네트워크는 마법이 아니라 아래 5단계의 결과입니다.
docker run -p 8080:80 nginx
│
① network namespace 생성 (컨테이너에 독립 네트워크 스택: 자체 lo·라우팅표·iptables)
│
② veth 쌍 생성 (가상 랜선 한 쌍)
│ ├─ 한쪽 끝 → 컨테이너 안 eth0
│ └─ 다른 끝 → 호스트 docker0 브리지에 꽂힘
│
③ IPAM이 컨테이너 eth0에 IP 할당 (172.17.0.0/16 에서 예: 172.17.0.2)
│ 기본 게이트웨이 = docker0 (172.17.0.1)
│
④ 나가는 트래픽 → iptables MASQUERADE(SNAT)
│ 출발지 IP 172.17.0.2 → 호스트 IP로 바꿔 내보냄 (그래야 응답이 돌아옴)
│
⑤ 들어오는 트래픽(-p 8080:80) → iptables DNAT
│ 호스트:8080 도착 → 목적지를 172.17.0.2:80 으로 바꿔 컨테이너로 전달
▼
[컨테이너 nginx가 응답] → 역방향으로 NAT을 되돌려 클라이언트에 도달
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① netns 생성 | 컨테이너에 호스트와 분리된 네트워크 스택(인터페이스·라우팅표·iptables)을 부여. --network host 면 이 단계를 건너뛰고 호스트 netns를 공유, none 이면 lo 만 둠 | host 모드는 격리가 없어 포트 충돌 · none 모드는 외부 통신 자체가 불가(의도된 격리) |
| ② veth 쌍 | 양끝이 이어진 가상 랜선을 만들어 한쪽은 컨테이너 eth0, 한쪽은 호스트 docker0 브리지에 연결. 호스트에서 ip link 에 veth... 로 보임 | 브리지에 안 꽂히면 컨테이너가 고립돼 외부·컨테이너 간 통신 불가 |
| ③ IP 할당(IPAM) | Docker의 IPAM이 브리지 서브넷에서 컨테이너 IP와 게이트웨이(docker0)를 배정 | 서브넷 고갈 → IP 할당 실패로 컨테이너 기동 오류 · 브리지 대역이 사내망과 겹치면 특정 목적지 라우팅이 어긋남 |
| ④ SNAT(MASQUERADE) | 컨테이너의 사설 IP를 호스트 IP로 위장(치환)해 밖으로 내보냄 — 그래야 외부가 돌려보낼 주소를 앎. 호스트의 net.ipv4.ip_forward=1 과 MASQUERADE 규칙이 전제 | ip_forward=0 이거나 MASQUERADE 규칙이 없으면 컨테이너에서 인터넷이 안 됨(핑도 안 나감) |
| ⑤ DNAT(-p 매핑) | 호스트 포트로 들어온 요청의 목적지를 컨테이너 IP:포트로 바꿔 전달. -p 하나가 iptables nat 테이블에 DNAT 규칙 한 줄로 박힘 | -p 를 안 줬거나, 앱이 컨테이너 안에서 127.0.0.1 에만 바인딩하면 DNAT가 전달해도 무응답(0.0.0.0 필요) |
같은 사용자 정의 네트워크(브리지)에 붙은 컨테이너끼리는 이 브리지를 L2 스위치처럼 거쳐 서로 직접 통신하고, 이때 이름→IP 해석은 각 컨테이너의 /etc/resolv.conf 에 박힌 임베디드 DNS 127.0.0.11 이 처리합니다(기본 bridge 에는 이 DNS가 없어 이름 통신 불가). host/none/bridge의 차이도 결국 ①에서 netns를 어떻게 두느냐의 차이입니다.
정리하면 "컨테이너 통신 성공"은 여러 층이 모두 참이라는 뜻입니다 — netns에 veth가 브리지로 이어졌고(②), IP·게이트웨이가 정상 배정됐고(③), 나갈 땐 SNAT·들어올 땐 DNAT가 걸려 있다(④⑤). 안 될 때는 컨테이너 안에서 ip addr·ip route 로 IP·게이트웨이를, 호스트에서 iptables -t nat -L -n 으로 MASQUERADE·DNAT 규칙을, sysctl net.ipv4.ip_forward 로 포워딩을 확인하면 어느 층에서 끊겼는지 좁혀집니다.
사용자 정의 네트워크와 내장 DNS
기본 브릿지(docker0)의 가장 큰 단점은 컨테이너 이름으로 통신이 불가능하다는 점입니다. IP는 재시작마다 바뀔 수 있기 때문에, 안정적인 서비스 간 통신을 위해서는 사용자 정의 네트워크를 만들어야 합니다.
사용자 정의 네트워크의 핵심 기능: 내장 DNS
사용자 정의 네트워크에는 Docker가 자동으로 **내장 DNS 서버(127.0.0.11)**를 제공합니다. 이 DNS 서버는 컨테이너 이름을 자동으로 해당 컨테이너의 IP로 변환해줍니다.
확대
사용자 정의 네트워크(my_network)는 내장 DNS 서버(127.0.0.11)를 제공합니다.
- web 컨테이너 (172.20.0.2) →
"web"이름으로 접근 가능 - db 컨테이너 (172.20.0.3) →
"db"이름으로 접근 가능
web 컨테이너 내부에서:
ping db # → 172.20.0.3으로 자동 해석
curl http://db:5432 # → 동작
기본 브릿지 vs 사용자 정의 네트워크 비교
두 네트워크 타입은 겉으로는 비슷해 보이지만 실무에서 차이가 확연합니다. 기본 브릿지는 테스트용으로는 충분하지만, Docker Compose로 여러 서비스를 연결하는 순간 사용자 정의 네트워크가 필수가 됩니다.
| 기능 | 기본 브릿지(docker0) | 사용자 정의 네트워크 |
|---|---|---|
| 컨테이너 이름 DNS | 불가 | 가능 |
| 자동 서비스 디스커버리 | 불가 | 가능 |
| 네트워크 격리 | 모든 컨테이너 공유 | 네트워크별 격리 |
| --link 의존성 | 필요(deprecated) | 불필요 |
| 컨테이너 별칭 설정 | 불가 | --network-alias 가능 |
네트워크 생성과 관리 명령어
# 사용자 정의 브릿지 네트워크 생성
docker network create my_net
# 서브넷과 게이트웨이를 명시적으로 지정
docker network create \
--driver bridge \
--subnet 172.20.0.0/16 \
--gateway 172.20.0.1 \
my_net
# 네트워크 상세 정보 확인
docker network inspect my_net
# 네트워크에 연결된 컨테이너 목록 확인
docker network inspect my_net --format '{{range .Containers}}{{.Name}} {{end}}'
# 사용하지 않는 네트워크 정리
먼저 기본 브릿지에서 컨테이너 이름으로 통신이 안 된다는 것을 직접 확인해봅니다.
실습 전 네트워크 실습용 디렉토리를 준비합니다.
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part2/network-demo && cd /tmp/docker/part2/network-demo
# 멀티 서비스 네트워크 구성 예제 파일 생성
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
frontend:
image: nginx:alpine
ports:
- "8081:80"
networks:
- frontend-net
backend:
image: python:3.11-slim
command: python -m http.server 8080
networks:
- frontend-net
- backend-net
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: secret
networks:
- backend-net
networks:
frontend-net:
backend-net:
EOF
1단계: 기본 네트워크에서 두 컨테이너 실행
# 터미널 1: 첫 번째 컨테이너 실행
docker run -d --name container-a alpine sleep 3600
# 터미널 2: 두 번째 컨테이너 실행
docker run -d --name container-b alpine sleep 3600
2단계: 각 컨테이너의 IP 확인
# container-a의 IP 확인
docker inspect container-a --format '{{.NetworkSettings.IPAddress}}'
# 예시 출력: 172.17.0.2
# container-b의 IP 확인
docker inspect container-b --format '{{.NetworkSettings.IPAddress}}'
# 예시 출력: 172.17.0.3
3단계: 이름으로 ping 시도 (실패 예상)
docker exec container-a ping -c 2 container-b
# ping: bad address 'container-b' ← 이름 해석 불가!
# IP로는 통신 가능
docker exec container-a ping -c 2 172.17.0.3
# PING 172.17.0.3: 56 data bytes — 성공
4단계: 정리
네트워크 실습 컨테이너 강제 삭제
안전한 실행 조건: container-a와 container-b가 이 모듈에서 만든 실습용 컨테이너인 경우에만 실행합니다.
실행 전 반드시 확인
- docker ps -a에서 두 컨테이너 이름을 확인했다
- 컨테이너 내부에 보존할 데이터가 없음을 확인했다
- 삭제 후 네트워크 실습을 다시 만들 수 있다
docker rm -f container-a container-b위 항목을 모두 확인한 후 복사할 수 있습니다
기본 브릿지에서는 IP가 아닌 이름으로 통신이 불가능함을 확인했습니다.
- 먼저 docker network inspect bridge 에서 Subnet(예: 172.17.0.0/16)과 Gateway를 확인한다 — 그 다음 Containers 항목에서 각 컨테이너의 IP를 읽는다. 기본 bridge는 172.17.x.x 대역, 사용자 정의 네트워크는 172.18.x.x 이상 대역을 자동 할당한다
- 기본 bridge에서 컨테이너 이름으로 ping하면 'bad address'가 나고, 같은 컨테이너를 사용자 정의 네트워크에 연결하면 이름으로 ping이 성공한다 — 이 차이가 내장 DNS 유무 때문이다. Docker Compose는 항상 사용자 정의 네트워크를 생성하므로 서비스 이름으로 통신이 가능하다
- host 네트워크 모드에서는 docker network inspect 에 컨테이너 항목이 나타나지 않고 포트 매핑(-p)도 무시된다 — host 모드 컨테이너의 포트는 호스트 포트와 동일하게 직접 노출된다. bridge 모드 대비 네트워크 격리가 없으므로 프로덕션 보안 서버에서는 신중하게 사용해야 한다
사용자 정의 네트워크를 만들어 컨테이너 이름으로 통신하는 방법을 실습합니다.
1단계: 사용자 정의 네트워크 생성
docker network create my_net
docker network ls
# my_net이 bridge 드라이버로 생성된 것 확인
2단계: 동일 네트워크에 컨테이너 연결
# --network my_net 으로 사용자 정의 네트워크에 연결
docker run -d --name web --network my_net nginx:alpine
docker run -d --name db --network my_net alpine sleep 3600
3단계: 컨테이너 이름으로 ping 통신
# db 컨테이너에서 web 컨테이너로 이름 기반 ping
docker exec db ping -c 3 web
# PING web (172.20.0.2): 56 data bytes
# 64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.123 ms ← 성공!
# web 컨테이너에서 db로도 통신 가능
docker exec web ping -c 3 db
# PING db (172.20.0.3): 56 data bytes — 성공
4단계: DNS 서버 확인
# 컨테이너 내부 DNS 설정 확인
docker exec db cat /etc/resolv.conf
# nameserver 127.0.0.11 ← Docker 내장 DNS
# options ndots:0
5단계: 네트워크 검사
docker network inspect my_net
# "Containers" 섹션에서 web, db 컨테이너 확인
6단계: 정리
docker network rm my_net
컨테이너를 다시 시작하지 않고 네트워크에 연결하거나 해제할 수 있습니다.
1단계: 컨테이너와 두 개의 네트워크 생성
docker network create net-frontend
docker network create net-backend
docker run -d --name api-server --network net-frontend alpine sleep 3600
docker run -d --name cache --network net-backend alpine sleep 3600
2단계: api-server를 backend 네트워크에도 연결
# 실행 중인 컨테이너에 네트워크 추가 연결
docker network connect net-backend api-server
# api-server는 이제 두 네트워크 모두에 연결됨
docker inspect api-server --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
# net-frontend net-backend
3단계: 통신 확인
# api-server에서 cache(net-backend에만 있음)로 통신
docker exec api-server ping -c 2 cache
# 성공 — api-server가 net-backend에 연결되어 있으므로
4단계: 네트워크 연결 해제
# api-server를 net-backend에서 분리
docker network disconnect net-backend api-server
# 이제 cache로의 통신 불가
docker exec api-server ping -c 2 cache
# ping: bad address 'cache'
5단계: 정리
docker network rm net-frontend net-backend
증상
두 컨테이너를 실행하고 서로 통신을 시도했는데, 컨테이너 이름으로 ping이 실패하거나 connection refused가 발생합니다.
docker exec app ping -c 2 database
# ping: bad address 'database'
원인 진단
1. 같은 네트워크에 있는지 확인
# 각 컨테이너가 어떤 네트워크에 연결되어 있는지 확인
docker inspect app --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}'
docker inspect database --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}'
두 컨테이너가 서로 다른 네트워크에 있으면 통신 불가.
2. 기본 브릿지(docker0) 사용 여부 확인
출력이 bridge(기본값)라면 사용자 정의 네트워크가 아닌 것입니다. 기본 브릿지에는 DNS가 없으므로 이름으로 통신 불가.
해결 방법
방법 1: 사용자 정의 네트워크로 재생성
docker network create my_app_net
docker run -d --name app --network my_app_net my_app_image
docker run -d --name database --network my_app_net postgres:14
방법 2: 실행 중인 컨테이너에 네트워크 연결
# 기존 컨테이너를 중단 없이 네트워크에 추가 연결
docker network connect my_app_net app
docker network connect my_app_net database
검증
docker exec app ping -c 2 database
# PING database: 성공
증상
--network host 옵션으로 컨테이너를 실행했는데, 호스트 IP로 접근이 안 됩니다.
docker run --network host nginx
# 브라우저에서 localhost:80 접근 시도 → 연결 거부
원인
Host 네트워크 모드는 리눅스 전용 기능입니다. macOS와 Windows에서 Docker Desktop은 리눅스 VM 위에서 동작하기 때문에, --network host를 사용해도 컨테이너가 공유하는 것은 VM의 네트워크이지 실제 macOS/Windows 호스트 네트워크가 아닙니다.
# macOS에서 확인
docker run --network host alpine ip addr
# 172.x.x.x 대역 출력 — macOS의 실제 네트워크가 아님
해결 방법
macOS/Windows 환경에서는 Host 모드 대신 포트 매핑을 사용합니다.
# Host 모드 대신 포트 매핑으로 대체
docker run -p 80:80 nginx
리눅스 서버(프로덕션)에서만 Host 모드의 성능 이점을 활용하고, 로컬 개발 환경(macOS/Windows)에서는 포트 매핑 방식을 사용하는 것이 권장됩니다.
환경 감지 스크립트 예시
OS_TYPE=$(uname -s)
if [ "$OS_TYPE" = "Linux" ]; then
NETWORK_FLAG="--network host"
else
NETWORK_FLAG="-p 8080:8080"
fi
docker run $NETWORK_FLAG my_service
실무에서는 서비스 유형에 따라 네트워크를 분리해 보안을 강화합니다. 외부에 노출해야 하는 서비스와 내부 서비스를 다른 네트워크에 배치하는 패턴이 일반적입니다.
실무 네트워크 구성 패턴
확대
실제 적용 예시
# 네트워크 생성
docker network create net-frontend
docker network create net-backend
docker network create net-data
# 프론트엔드 레이어
docker run -d --name nginx --network net-frontend -p 80:80 nginx
docker run -d --name web-app --network net-frontend my_frontend:latest
# API 게이트웨이 — 두 네트워크에 모두 연결 (브릿지 역할)
docker run -d --name api-gateway --network net-frontend my_gateway:latest
docker network connect net-backend api-gateway
# 백엔드 서비스
docker run -d --name auth-service --network net-backend my_auth:latest
docker run -d --name product-service --network net-backend my_product:latest
# 데이터 레이어
docker run -d --name postgres --network net-data postgres:14
# product-service를 data 네트워크에도 연결
docker network connect net-data product-service
이 패턴의 보안상 이점
- nginx는 인터넷에 노출되어 있지만, 직접 DB에 접근 불가
- postgres는 data 네트워크에만 있어서 외부에서 직접 접근 불가
- 침해사고 발생 시 피해 범위가 해당 네트워크로 제한됨
- Docker Compose의
networks섹션으로 이 구성을 코드로 관리 가능
현업 DevOps 엔지니어는 이런 네트워크 분리를 Docker Compose 파일이나 Kubernetes NetworkPolicy로 선언적으로 관리합니다.
증상
두 컨테이너가 동일한 사용자 정의 네트워크에 있고 ping도 성공하는데, 특정 포트로 연결이 되지 않습니다.
docker exec web curl http://api:8080/health
# curl: (7) Failed to connect to api port 8080: Connection refused
원인 진단
1. 컨테이너가 실제로 해당 포트를 열고 있는지 확인
# api 컨테이너에서 실제 리스닝 포트 확인
docker exec api netstat -tlnp 2>/dev/null || docker exec api ss -tlnp
# tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN (포트 번호 확인)
포트가 8080이 아닌 3000으로 열려 있다면 잘못된 포트를 지정한 것입니다.
2. 애플리케이션이 루프백에만 바인딩되어 있는지 확인
docker exec api ss -tlnp
# tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN
# ↑ 루프백(127.0.0.1)에만 바인딩 → 외부 접근 불가!
애플리케이션이 127.0.0.1:8080에 바인딩되어 있으면 같은 컨테이너 내부에서만 접근 가능합니다. 다른 컨테이너에서 접근하려면 0.0.0.0:8080으로 바인딩해야 합니다.
해결 방법
애플리케이션 설정 변경 (루프백 → 모든 인터페이스)
# Node.js 예시 — 0.0.0.0으로 바인딩 변경
app.listen(8080, '0.0.0.0', () => {
console.log('서버 시작: 0.0.0.0:8080');
});
# Python FastAPI 예시
uvicorn main:app --host 0.0.0.0 --port 8080
확인 방법
# 변경 후 바인딩 재확인
docker exec api ss -tlnp
# tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN ← 올바름
# 연결 테스트
docker exec web curl http://api:8080/health
# {"status": "ok"} ← 성공!
포트 번호 불일치인 경우
# EXPOSE는 문서화 목적일 뿐, 실제 포트와 다를 수 있음
docker inspect api --format '{{.Config.ExposedPorts}}'
# map[8080/tcp:{}] — Dockerfile의 EXPOSE 값
# 실제 리스닝 포트는 별도 확인
docker exec api ss -tlnp | grep LISTEN
심화 — 인터넷은 되는데 특정 사내 IP로만 안 갈 때
심화: Docker 기본 주소 풀과 사내망 충돌 — 라우팅이 브릿지로 새는 함정
컨테이너에서 인터넷은 되는데 사내 서버 하나로만 연결이 안 된다는 증상은 DNS나 포트 문제가 아니라 서브넷이 겹쳐 라우팅이 어긋난 경우가 많습니다. 사용자 정의 네트워크의 IP 대역이 어디서 오는지 알아야 보입니다.
- 서브넷은 자동으로 할당됩니다: docker network create에 --subnet을 주지 않으면, Docker는 기본 주소 풀(default-address-pools, 대개 172.17.0.0/16부터 172.31, 그 뒤 192.168 대역)에서 안 쓰는 대역을 하나 떼어 줍니다.
- 사내망과 겹치면 라우팅이 샙니다: 자동으로 잡힌 이 대역이 회사 사내망이나 VPN 대역과 겹치면, 컨테이너의 라우팅 테이블에서 그 대역이 로컬 브릿지(eth0) 로 향합니다. 그래서 같은 대역의 진짜 사내 서버로 나가야 할 패킷이 브릿지 안에서 맴돌고, 실제 게이트웨이로 못 나갑니다.
- 호스트는 멀쩡한데 컨테이너만 막힙니다: 호스트의 라우팅은 그대로라 호스트에서는 사내 서버에 잘 붙습니다. 오직 겹치는 대역에 놓인 컨테이너만 그 목적지를 못 봅니다. 인터넷(다른 대역)은 정상이라 원인 짐작이 더 어렵습니다.
- 대역을 비켜 주면 됩니다: daemon.json의 default-address-pools를 사내망과 안 겹치는 대역으로 지정하거나, 문제 네트워크를 --subnet으로 겹치지 않게 다시 만듭니다.
컨테이너 네트워크를 설계할 때 어떤 사설 대역을 쓰는지를 사내망과 대조하는 것이, 나중의 특정 목적지만 안 되는 사고를 막습니다.
상황: 컨테이너 안에서 외부 인터넷은 문제없이 나가는데, 사내 DB 서버(예: 같은 사내 대역의 특정 IP)로만 접속이 안 됩니다. 같은 호스트의 셸에서 그 서버로 붙으면 정상입니다.
원인: Docker가 사용자 정의 네트워크에 자동 할당한 브릿지 서브넷이 그 사내 서버가 속한 대역과 겹쳤습니다. 컨테이너 입장에서 그 대역은 바로 옆 로컬 네트워크라, 패킷을 실제 사내 게이트웨이로 보내지 않고 브릿지 쪽으로 처리해 도달하지 못합니다.
진단: docker network inspect로 각 네트워크의 Subnet 값을 확인해, 연결이 안 되는 대상 IP가 그 대역 안에 들어가는지 봅니다. 컨테이너 안에서 ip route를 찍어 대상 대역이 기본 게이트웨이가 아니라 eth0(브릿지)로 향하는지 확인하고, 호스트의 ip route와 비교합니다.
해결: /etc/docker/daemon.json의 default-address-pools를 사내망·VPN과 겹치지 않는 대역으로 바꾸고 Docker를 재시작한 뒤, 문제 네트워크를 재생성합니다. 급하면 해당 네트워크만 --subnet으로 겹치지 않는 대역을 지정해 다시 만듭니다. --network host로 우회할 수도 있지만 이는 격리를 포기하는 임시책일 뿐 근본 해결이 아닙니다(ip/ifconfig 명령어로 네트워크 카드 정보 파악 및 라우팅 테이블).
네트워크 디버깅 — 컨테이너 통신이 안 될 때 진단하는 법
두 컨테이너가 같은 Compose 파일에 있는데 서로 통신이 안 됩니다. 에러 로그에는 "Connection refused" 또는 "No such host"가 뜹니다. 네트워크 문제는 원인이 여러 개일 수 있어서 무작정 설정을 바꾸면 오히려 혼란이 커집니다. 컨테이너가 같은 네트워크에 속해 있는지, DNS가 이름을 올바르게 해석하는지, 포트가 실제로 열려 있는지 순서대로 좁혀 나가야 빠르게 찾을 수 있습니다. 이 ConceptBlock에서는 컨테이너 간 통신 문제를 5단계로 진단하는 방법을 다룹니다.
확대
단계별 진단 절차
컨테이너 간 통신 문제는 대부분 이 5단계 절차로 원인을 찾을 수 있습니다.
Step 1: 두 컨테이너가 같은 네트워크에 있는지 확인
# 네트워크 상세 정보 확인 — Containers 섹션에 두 컨테이너 모두 있어야 함
docker network inspect my_net
# 출력 예시
{
"Name": "my_net",
"Containers": {
"abc123": { "Name": "web", "IPv4Address": "172.20.0.2/16" },
"def456": { "Name": "db", "IPv4Address": "172.20.0.3/16" }
}
}
# 특정 컨테이너가 속한 네트워크 목록 확인
docker inspect web --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
Step 2: 컨테이너 내부에서 DNS 해석 테스트
# nslookup으로 이름 → IP 변환 테스트
docker exec web nslookup db
# Server: 127.0.0.11 ← Docker 내장 DNS
# Address: 127.0.0.11#53
# Name: db
# Address: 172.20.0.3 ← 정상 해석됨
# ping으로 연결 확인
docker exec web ping -c 3 db
# Alpine 기반 이미지는 nslookup 대신 nslookup 패키지 설치 필요
# docker exec -it web sh
# / # nslookup db || apk add --no-cache bind-tools
Step 3: 포트 연결 테스트
# nc(netcat)으로 TCP 포트 열림 확인
docker exec web nc -zv db 5432
# Connection to db 5432 port [tcp/postgresql] succeeded!
# wget으로 HTTP 응답 확인
docker exec web wget -qO- http://api:8080/health
# curl 사용 시
docker exec web curl -s http://api:8080/health
Step 4: 컨테이너 네트워크 인터페이스 확인
# 컨테이너 내부 네트워크 인터페이스
docker exec web ip addr show
# eth0: 172.20.0.2/16 ← 컨테이너 IP
# 라우팅 테이블 확인
docker exec web ip route
# default via 172.20.0.1 dev eth0 ← 게이트웨이
자주 발생하는 네트워크 문제
| 증상 | 원인 | 해결 |
|---|---|---|
ping db 실패 | 다른 네트워크에 있음 | 같은 network에 두 컨테이너 연결 |
| DNS 해석 안 됨 | 기본 bridge(docker0) 사용 중 | 사용자 정의 network로 변경 |
ping 성공, curl :80 실패 | 앱이 127.0.0.1만 리스닝 | 앱 바인딩 주소를 0.0.0.0으로 변경 |
| 컨테이너 재시작 후 IP 변경 | 기본 bridge 사용 | 사용자 정의 network + 이름으로 통신 |
| Compose 서비스 간 통신 안 됨 | 다른 Compose 프로젝트 | external network 공유 설정 |
기본 bridge vs 사용자 정의 network 한 눈에 비교
# 기본 bridge (docker0): 이름으로 통신 불가
docker run -d --name app1 nginx
docker run -d --name app2 nginx
docker exec app1 ping app2 # ❌ 실패! IP 직접 입력해야 함
# 사용자 정의 network: 이름으로 통신 가능
docker network create mynet
docker run -d --name app1 --network mynet nginx
docker run -d --name app2 --network mynet nginx
docker exec app1 ping app2 # ✅ 성공! 이름으로 통신됨
Docker Compose와 네트워크 — 자동 생성 네트워크 이해
docker-compose.yml에 networks를 한 번도 선언하지 않았는데 서비스 이름으로 통신이 됩니다. 왜 그럴까요? Compose는 프로젝트마다 자동으로 기본 네트워크를 만들고 모든 서비스를 거기에 붙입니다. 이 동작을 모르면 "Compose에서는 됐는데 왜 docker run으로 따로 뜬 컨테이너와는 통신이 안 되지?"라는 혼동이 생깁니다. 자동 생성 네트워크의 이름 규칙과 서비스 간 연결 방식을 이해하면, 다중 Compose 프로젝트를 연결하거나 외부 컨테이너를 편입하는 고급 패턴도 자연스럽게 납득됩니다. 이 ConceptBlock에서는 Compose가 네트워크를 자동으로 구성하는 원리를 다룹니다.
확대
Compose가 네트워크를 자동으로 만드는 이유
docker-compose.yml에 networks를 명시하지 않아도 서비스 간 통신이 되는 이유가 있습니다. Compose는 프로젝트마다 프로젝트명_default 네트워크를 자동으로 만들고 모든 서비스를 여기에 연결합니다.
# myapp 디렉토리에서 docker compose up 실행
cd ~/myapp && docker compose up -d
# 자동 생성된 네트워크 확인
docker network ls
# NETWORK ID NAME DRIVER
# abc123def456 myapp_default bridge ← 자동 생성됨
# ↑ 디렉토리명이 프로젝트명이 됨
이 네트워크 안에서 서비스 이름이 곧 DNS 이름이 됩니다.
# 네트워크를 명시하지 않아도 서비스 이름으로 통신 가능
services:
web:
image: myapp
environment:
- DB_HOST=db # 'db' = 서비스 이름 = DNS 이름
db:
image: postgres:16-alpine
여러 Compose 스택 간 네트워크 공유
서로 다른 docker-compose.yml 파일로 실행한 서비스들이 통신해야 할 때, external network를 사용합니다.
# 1. 공유 네트워크 먼저 생성
docker network create shared-net
# stack-a/compose.yml
networks:
shared-net:
external: true # 외부에서 이미 만든 네트워크 사용
services:
api:
image: my-api
networks:
- shared-net
- default # stack-a 내부 통신용
# stack-b/compose.yml
networks:
shared-net:
external: true
services:
frontend:
image: my-frontend
networks:
- shared-net # api 서비스에 접근 가능
docker compose -f stack-a/compose.yml up -d
docker compose -f stack-b/compose.yml up -d
# frontend에서 api로 통신 가능
docker exec stack-b-frontend-1 curl http://api:8080/health
네트워크 격리 전략
services:
web:
networks:
- frontend # 외부와 통신
- backend # DB와 통신
db:
networks:
- backend # backend에만 존재 → 외부 직접 접근 불가
# frontend 네트워크 없음 → web을 통해서만 접근 가능
redis:
networks:
- backend
networks:
frontend: # 외부 트래픽 용
backend: # 내부 서비스 전용 (DB, 캐시 등)
이렇게 네트워크를 분리하면 DB와 캐시는 web 서비스를 통해서만 접근 가능하여 보안이 강화됩니다.
정리
Docker 네트워크의 핵심 원칙을 기억하세요.
| 상황 | 권장 선택 |
|---|---|
| 단일 호스트 컨테이너 간 통신 | 사용자 정의 브릿지 네트워크 |
| 최고 성능이 필요한 리눅스 서버 | Host 네트워크 |
| 네트워크 완전 격리 필요 | None 네트워크 |
| 이름 기반 서비스 디스커버리 | 사용자 정의 네트워크(내장 DNS) |
기본 브릿지(docker0)는 테스트 용도로만 사용하고, 실제 서비스는 항상 docker network create로 만든 사용자 정의 네트워크를 사용하세요. 이름 기반 통신이 가능해져 설정 관리가 훨씬 쉬워집니다.
자주 사용하는 네트워크 관련 명령어
# 네트워크 목록 확인
docker network ls
# 네트워크 생성
docker network create my_net
# 네트워크 상세 정보 (연결된 컨테이너, IP 대역 확인)
docker network inspect my_net
# 실행 중인 컨테이너를 네트워크에 연결
docker network connect my_net my_container
# 컨테이너를 네트워크에서 분리
docker network disconnect my_net my_container
# 사용하지 않는 네트워크 일괄 삭제
# 컨테이너가 연결된 네트워크 확인
docker inspect my_container --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
핵심 요약의 네트워크 일괄 정리
안전한 실행 조건: 삭제 대상이 컨테이너에 연결되지 않은 임시 네트워크뿐인 경우에만 실행합니다.
실행 전 반드시 확인
- docker network ls와 docker network inspect로 삭제 대상을 확인했다
- Compose 프로젝트가 사용하는 네트워크가 아님을 확인했다
- 다시 생성할 네트워크 이름과 설정을 알고 있다
docker network prune위 항목을 모두 확인한 후 복사할 수 있습니다
네트워크 설계 시 보안과 격리를 위해 서비스 유형(프론트엔드/백엔드/데이터)별로 별도 네트워크를 만들고, 필요한 컨테이너만 각 네트워크에 연결하는 최소 권한 원칙을 적용하세요.
명령어·단축키 빠른 참조
이 모듈에서 다룬 Docker 네트워크 명령과 컨테이너 통신 진단 명령을 실전 예와 함께 모았습니다. "예" 열은 그대로 복사해 써도 됩니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
docker network ls | 네트워크 목록·드라이버 확인 | 기본 bridge/host/none과 사용자 정의 네트워크 구분 |
docker network create | 사용자 정의 bridge 생성(내장 DNS 제공) | docker network create --subnet 172.20.0.0/16 my_net |
docker network inspect | 서브넷·연결된 컨테이너 확인 | docker network inspect my_net --format '{{range .Containers}}{{.Name}} {{end}}' |
docker network connect | 실행 중 컨테이너를 네트워크에 추가 연결 | docker network connect net-backend api-server (재시작 없이) |
docker network disconnect | 네트워크에서 분리 | docker network disconnect net-backend api-server |
docker network rm | 사용자 정의 네트워크 삭제 | docker network rm net-frontend net-backend |
docker network prune | 미사용 네트워크 일괄 정리 | 연결된 컨테이너 없는 네트워크만 삭제(주의) |
docker run --network | 컨테이너를 특정 네트워크로 실행 | --network my_net(이름 통신) · --network host · --network none |
docker exec <c> ping <이름> | 이름 기반 연결 확인 | docker exec db ping -c 3 web (사용자 정의 net에서만 이름 해석) |
docker exec <c> nslookup <이름> | 내장 DNS 이름 해석 테스트 | docker exec web nslookup db → 127.0.0.11이 응답 |
docker exec <c> nc -zv <이름> <포트> | 포트 연결 확인 | docker exec web nc -zv db 5432 (ping은 되는데 포트가 안 될 때) |
docker exec <c> ip route | 컨테이너 라우팅·게이트웨이 확인 | 사내망 대역과 브릿지 서브넷 충돌 진단 |
docker inspect <c> --format '{{...Networks}}' | 컨테이너가 속한 네트워크 확인 | docker inspect web --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' |
관련 모듈로 더 깊이:
- 복잡한 멀티 서비스 환경의 유기적 연동과 배포 — 사용자 정의 네트워크를 YAML로 선언하고 서비스 이름으로 자동 연결하는 법
- 프로덕션 수준의 멀티 서버 아키텍처 배포 워크플로우 — 네트워크 분리를 활용해 Nginx·WAS·DB를 격리 배치하는 실전 구성
- 컨테이너 환경에서 처음 마주하는 흔한 오류 5가지 극복기 — 컨테이너 간 통신이 안 될 때 네트워크 관점에서 진단하는 법
- ip/ifconfig 명령어로 네트워크 카드 정보 파악 및 라우팅 테이블 — 컨테이너 네트워크가 올라타는 리눅스 IP·서브넷·게이트웨이의 기초 (Linux 트랙 · 기반 개념)
다음 모듈에서는 여러 컨테이너, 네트워크, 볼륨 구성을 Docker Compose 파일 하나로 선언하고 한 번에 실행하는 방법을 다룹니다.