[GDSC] Docker 네트워크 | 컨테이너는 어떻게 서로 통신할까?
이전까지는 이미지, 컨테이너, 볼륨을 다루며 “컨테이너 내부의 데이터”에 집중했다. 그런데 실제 서비스 환경에서는 컨테이너가 하나만 존재하는 경우는 거의 없다.
Node.js 서버, MongoDB 데이터베이스, Redis 캐시... 이 세 가지를 전부 같은 컨테이너 안에 넣을 순 없다. 역할이 다르고, 라이프사이클도 다르기 때문이다.
그래서 여러 개의 컨테이너를 서로 연결해 하나의 애플리케이션처럼 동작하게 만들어야 한다. 그 중심에 있는 개념이 바로 Docker 네트워크(Docker Network) 다.
컨테이너에서 외부 웹으로
강의 예제는 Node.js 기반의 웹 API 애플리케이션이다.
express, axios, mongoose를 사용하고 있으며 /movies, /people, /favorites 등의 엔드포인트를 제공한다.
/movies와 /people: Star Wars API(swapi.dev)로 GET 요청을 보내고 받은 데이터를 그대로 반환한다.
/favorites: MongoDB에 데이터를 저장하거나 조회하는 엔드포인트
먼저, 컨테이너 내부의 애플리케이션이 외부 웹 API로 요청을 보낼 수 있을까?
docker build -t favorites-node .
docker run -d --name favorites -p 3000:3000 favorites-node
이렇게 띄운 후 Postman에서 GET http://localhost:3000/movies를 보내면
Star Wars API의 응답이 그대로 반환된다.
즉, 컨테이너로 실행해도 외부 API 요청은 문제없이 작동한다.
왜 이런 게 가능할까? 컨테이너는 가상의 환경인데, 어떻게 외부 인터넷에 나갈 수 있을까?
Docker는 기본적으로 컨테이너에 아웃바운드 네트워크 접근 권한을 부여하기 때문이다. 따라서 이미지를 빌드하고 컨테이너를 띄우면, 컨테이너 내부의 애플리케이션은 웹 API로 자유롭게 요청을 보낼 수 있다.
즉, 별도의 네트워크 설정 없이 컨테이너 내부에서 외부 인터넷으로의 요청은 기본적으로 허용된다.
Docker는 컨테이너 내부의 사설 IP(172.17.0.x)를 호스트의 실제 IP로 변환해 외부로 보내는 NAT 구조를 사용한다. 쉽게 말하면, 집 안의 여러 기기가 하나의 공유기를 통해 인터넷을 사용하는 것과 같다. docker0 브리지가 바로 그 “공유기” 역할을 맡는다. 덕분에 각 컨테이너는 인터넷에 직접 노출되지 않으면서도 외부 API에 안전하게 접근할 수 있다.
컨테이너에서 호스트로
이번에는 반대 방향이다. 컨테이너 내부에서 호스트(내 컴퓨터)에 설치된 MongoDB 데이터베이스에 접근하려고 하면, 연결 문자열에 단순히 localhost를 넣어서는 동작하지 않는다. 컨테이너 입장에서 localhost는 자기 자신, 즉 컨테이너 내부를 가리키기 때문이다.
이때 Docker가 제공하는 특별한 주소가 있다.
바로 host.docker.internal 이다.
이 도메인은 컨테이너 내부에서 호스트 머신의 IP 주소로 자동 변환된다.
따라서 아래처럼 MongoDB 연결 코드를 수정하면 컨테이너에서 호스트로 접근이 가능하다.
// 잘못된 예
mongoose.connect("mongodb://localhost:27017/favorites");
// 올바른 예
mongoose.connect("mongodb://host.docker.internal:27017/favorites");
이후 이미지를 다시 빌드하고 컨테이너를 실행하면, 컨테이너 내부의 Node.js 앱이 호스트에 설치된 MongoDB로 정상적으로 연결된다.
docker build -t favorites-node .
docker run -d --rm --name favorites -p 3000:3000 favorites-node
Postman에서 /favorites 엔드포인트에 요청을 보내면
로컬 MongoDB에 저장된 데이터가 반환되는 것을 확인할 수 있다.
즉, 컨테이너 내부에서 호스트의 서비스와 통신하고 싶다면
host.docker.internal을 사용하면 된다.
컨테이너에서 컨테이너로
다음 단계는 MongoDB도 컨테이너로 분리하는 것이다. 공식 Mongo 이미지를 그대로 사용하면 된다.
docker run -d --name mongodb mongo
이제 Node.js 컨테이너가 이 MongoDB 컨테이너에 연결해야 한다.
처음엔 docker container inspect mongodb 명령으로 IP 주소를 확인해 연결할 수도 있다.
docker container inspect mongodb
# "NetworkSettings": { "IPAddress": "172.xx.xx.xx" }
하지만 이 방식은 매번 IP를 확인해야 하고, 컨테이너가 재시작되면 IP가 바뀌므로 현실적인 방법이 아니다.
이 문제를 해결하는 방법은 사용자 정의 네트워크를 생성하는 것이다.
docker network create favorites-net
그 다음 두 컨테이너를 같은 네트워크에 포함시킨다.
docker run -d --name mongodb --network favorites-net mongo
docker run -d --name favorites --network favorites-net -p 3000:3000 favorites-node
이제 Node.js 앱의 연결 코드를 아래처럼 수정할 수 있다.
mongoose.connect("mongodb://mongodb:27017/favorites");
이때 mongodb는 컨테이너 이름이다.
Docker는 사용자 정의 네트워크를 만들면, 그 안에서 자체 DNS 서버를 자동으로 실행한다. 같은 네트워크 안의 컨테이너들은 서로를 이름으로 인식할 수 있다.
즉, mongodb라는 이름을 입력하면 Docker DNS가 자동으로 172.18.0.2 같은 IP로 변환해준다. 이제 더 이상 IP를 기억할 필요가 없다!
이제 Node.js 컨테이너가 MongoDB 컨테이너와 안정적으로 통신할 수 있다.
컨테이너 네트워크의 작동 원리
이제 한 걸음 더 들어가 보자. 컨테이너가 실제로 어떻게 연결되어 통신하는지 내부 구조를 살펴보면 이해가 훨씬 쉬워진다.
🔹veth와 eth0 — 컨테이너의 가상 케이블
컨테이너가 생성될 때 Docker는 veth (virtual ethernet) 라는 가상 네트워크 인터페이스를 호스트에 자동 생성한다. 이 veth는 컨테이너 내부의 eth0 인터페이스와 1:1로 연결되어 있다.
즉, veth는 “호스트 ↔ 컨테이너”를 잇는 가상의 케이블이다. 컨테이너 안에서 데이터가 eth0로 나가면, 호스트에서는 vethXXXX 인터페이스를 통해 docker0 브리지로 전달된다.
🔹docker0 — 컨테이너 세계의 허브
호스트에는 docker0라는 가상 브리지가 자동으로 생성된다. 이 브리지는 모든 컨테이너의 veth 인터페이스를 연결해주는 중앙 허브이자 게이트웨이다.
docker0는 기본적으로 172.17.0.1 주소를 가지고 있으며,
첫 번째 컨테이너에는 172.17.0.2, 두 번째에는 172.17.0.3 같은 IP가 순차적으로 할당된다.
이 덕분에 서로 다른 컨테이너들이 내부 IP 기반으로 자유롭게 통신할 수 있고, 외부 인터넷 요청은 NAT를 통해 나간다.
🔹브리지 네트워크를 새로 만들면?
docker0는 기본 브리지지만, 직접 사용자 정의 브리지를 만들 수도 있다.
docker network create --driver bridge mybridge
이때 docker0가 172.17.0.x를 사용 중이라면 새로운 브리지는 자동으로 172.18.0.x 대역을 할당받는다.
이렇게 독립된 네트워크를 생성하면 프로젝트별로 격리된 네트워크 환경을 구성할 수 있다.
포트 매핑은 언제 필요한가?
여기서 헷갈리기 쉬운 부분이 바로 포트 매핑이다.
-p 3000:3000 옵션은 호스트나 외부에서 접근할 때만 필요하다.
예를 들어 브라우저나 Postman이 localhost:3000으로 Node.js 컨테이너에 접근하려면
이 포트가 열려 있어야 한다.
하지만 컨테이너 간 통신의 경우에는 불필요하다. 같은 네트워크 내에서는 Docker가 내부적으로 모든 포트를 자동 관리하기 때문이다. 따라서 MongoDB 컨테이너는 -p 옵션 없이 실행해도 된다.
정리
-
컨테이너에서 외부 웹으로의 요청은 기본적으로 허용된다.
-
컨테이너에서 호스트로 접근하려면 host.docker.internal을 사용한다.
-
컨테이너 간 통신은 사용자 정의 네트워크를 만들어 컨테이너 이름으로 연결한다.
-
포트 매핑(-p)은 외부 접근이 필요한 컨테이너에만 설정한다.