Skip to content
ioob.dev
Go back

Docker 입문 6편 — 네트워크

· 10분 읽기
Docker 시리즈 (6/13)
  1. Docker 입문 1편 — Docker란
  2. Docker 입문 2편 — 이미지와 레이어
  3. Docker 입문 3편 — Dockerfile 작성법
  4. Docker 입문 4편 — 컨테이너 생명주기
  5. Docker 입문 5편 — 볼륨과 데이터 영속성
  6. Docker 입문 6편 — 네트워크
  7. Docker 7편 — Docker Compose로 다중 컨테이너 오케스트레이션
  8. Docker 8편 — 멀티스테이지 빌드로 이미지 다이어트
  9. Docker 9편 — 레지스트리, 이미지는 어디에 두는가
  10. Docker 10편 — 컨테이너 보안, 터지기 전에 막는 것들
  11. Docker 11편 — BuildKit과 고급 빌드
  12. Docker 12편 — 프로덕션 모범 사례
  13. Docker 13편 — 트러블슈팅과 대안
Table of contents

Table of contents

컨테이너는 서로 어떻게 이야기하는가

컨테이너 하나만 띄울 때는 네트워크를 의식할 일이 없다. docker run -p 8080:80 nginx 한 줄이면 호스트의 8080으로 접속이 된다. 그런데 컨테이너가 둘, 셋이 되고, DB와 앱과 캐시가 각각 다른 컨테이너에 담기기 시작하면 갑자기 질문들이 쏟아진다.

이 편에서는 이 모든 질문에 답한다. 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의 이점은 여러 가지다.

# 컨테이너를 네트워크에 추가/제거
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 스트리밍, 고성능 프록시, 레이턴시에 민감한 서비스에서 유용하다.

단점도 크다.

특별한 이유가 없다면 host보다 bridge + -p가 낫다. 성능이 정말 절실하거나 호스트 네트워크에 직접 바인딩해야 할 때만 쓴다.

none — 네트워크를 꺼버린다

--network none은 컨테이너에 네트워크를 아예 달지 않는다. loopback(lo)만 있고, 외부 통신 불가.

docker run --rm --network none alpine ip addr
# 1: lo: <LOOPBACK> 만 보임

쓰임새는 협소하지만 두 가지 상황에서 유용하다.

  1. 강한 격리가 필요한 배치 작업 (외부 통신이 필요 없는 계산)
  2. 커스텀 네트워크를 직접 구성하려는 특수 시나리오 (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 규칙 한 줄이 트래픽을 돌린다. 정확히는 다음이 일어난다.

  1. 외부에서 호스트 0.0.0.0:8080으로 TCP 요청 도착
  2. iptables가 패킷의 목적지를 컨테이너 IP의 80 포트로 바꿈 (Destination NAT)
  3. bridge 네트워크를 통해 컨테이너 안으로 전달
  4. 응답 경로에선 반대로 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의 차이를 정리한다. 실무 선택에 바로 쓸 수 있다.

항목기본 bridgeuser-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는 이렇게 한다.

  1. <프로젝트명>_default라는 user-defined bridge를 만든다
  2. 세 서비스를 이 네트워크에 모두 붙인다
  3. 각 서비스명이 그대로 DNS 호스트명이 된다 (db, cache, api)
  4. 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로 여러 컨테이너를 묶어 운영하는 방법을 다룬다.

7편: Docker Compose


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Docker 입문 5편 — 볼륨과 데이터 영속성
Next Post
Docker 7편 — Docker Compose로 다중 컨테이너 오케스트레이션