7편에서 dev/prod 컨테이너를 구분했다면, 이번 글은 그 컨테이너들이 실제로 어떤 경로를 통해 통신하는지 정리합니다. 목표는 단순합니다. 호스트 포트, 컨테이너 포트, 서비스 이름을 명확히 구분해 머릿속에 그림을 그리는 것. 예제는 nginx 웹, Node API, PostgreSQL DB 세 컨테이너를 사용합니다.
이 글의 흐름
- 호스트/컨테이너/서비스 이름을 구분하는 머릿속 다이어그램
- 웹·API·DB Compose 스니펫으로 포트와 네트워크 읽기
- 서비스 이름으로 내부 DNS가 어떻게 작동하는지 이해하기
- Mini Lab: curl과 ping으로 통신 경로 확인하기
- 가장 많이 헷갈리는 주소를 한 번에 정리하기
읽기 카드
- 예상 소요 시간: 17분
- 사전 준비:
docker compose up과 포트 개념 기초- 읽고 나면: 포트 매핑과 서비스 이름을 초보자에게 직접 설명할 수 있습니다.
머릿속 다이어그램
아래 텍스트 다이어그램을 계속 떠올리면 헷갈릴 일이 거의 없습니다.
[브라우저] --localhost:8080--> [호스트 OS] --포트 매핑--> [web 컨테이너 80]
|
+--> 같은 네트워크의 다른 컨테이너 (service name)
- 왼쪽(브라우저)은 항상 localhost + 공개 포트로 접속합니다.
- 가운데(포트 매핑)는
ports: "8080:80"처럼 "호스트" 순서로 연결합니다. - 오른쪽(컨테이너)에서는 서비스 이름(
api,db등)을 DNS처럼 사용해 서로 통신합니다.
세 구간을 마음속으로 구분하면 8080과 80, api:4000과 localhost:4000 같은 주소가 헷갈리지 않습니다.
웹·API·DB Compose 스니펫
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./public:/usr/share/nginx/html:ro
depends_on:
- api
networks:
- app-net
api:
build: ./api
environment:
- DATABASE_URL=postgres://postgres:your-db-password@db:5432/app
ports:
- "4000:4000"
networks:
- app-net
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=your-db-password
- POSTGRES_DB=app
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-net
volumes:
db-data:
networks:
app-net:
driver: bridge
web은 외부8080을 내부80에 연결하고, 정적 파일만 읽어 옵니다.api는 외부에서도 디버깅할 수 있도록4000:4000을 열어 두었습니다.db는 포트 매핑이 없습니다. 즉, 호스트에서 바로 접근할 수 없고 같은 네트워크 컨테이너만db:5432로 접속할 수 있습니다.DATABASE_URL에db:5432가 등장합니다. 여기서db는 컨테이너 이름이 아니라 서비스 이름 기반 DNS 기록입니다.
추가로, api 컨테이너 안의 서버 프로그램은 보통 0.0.0.0에 바인딩되어 있어야 다른 컨테이너나 호스트에서 접근할 수 있습니다. 초보자들이 localhost에만 바인딩해 두고 "왜 포트를 열었는데도 안 되지?" 하고 헷갈리는 경우가 많습니다.
서비스 이름 = 내부 DNS
Compose는 같은 네트워크에 묶인 서비스마다 service-name 호스트를 자동으로 만들어 줍니다.
| 하고 싶은 일 | 사용하는 주소 |
|---|---|
| 브라우저에서 웹 확인 | http://localhost:8080 |
| 호스트에서 API 호출 | http://localhost:4000 |
| web 컨테이너가 API 호출 | http://api:4000 |
| API 컨테이너가 DB 접속 | postgres://...@db:5432/app |
localhost는 언제나 내 컴퓨터를 가리킵니다. 컨테이너끼리 서로 부르는 이름은 서비스 이름입니다. 두 주소를 혼동하면 대부분의 통신 오류가 발생합니다.
즉, 호스트에서는 localhost, 같은 Compose 네트워크 안에서는 서비스 이름을 쓴다고 생각하면 됩니다. 호스트에서 api:4000처럼 바로 접속할 수는 없습니다.
Mini Lab: 통신 경로 확인하기
- 위 Compose 파일을 저장하고
docker compose up -d를 실행합니다. curl localhost:8080으로 웹 정적 파일이 보이는지 확인합니다.curl localhost:4000/health으로 API가 응답하는지 확인합니다.docker compose exec web ping -c 1 api를 실행해 web 컨테이너에서 API를 서비스 이름으로 찾을 수 있는지 확인합니다.docker compose exec api sh -c "nc -vz db 5432"로 API 컨테이너가 DB 포트에 접속 가능한지 테스트합니다.docker compose down으로 정리합니다.
이 과정을 반복하면 "내가 지금 어디에서 어디로 접속하는지"를 자연스럽게 설명할 수 있게 됩니다.
실패 예시도 한 번 보면 더 오래 기억됩니다.
docker compose exec web curl http://localhost:4000/health를 실행하면, 많은 경우 web 컨테이너 자기 자신의localhost를 바라보기 때문에 API에 닿지 못합니다.- 반대로
curl http://api:4000/health처럼 서비스 이름을 쓰면 같은 네트워크 안에서 API 컨테이너를 찾을 수 있습니다.
즉, 컨테이너 안에서의 localhost는 "내 맥"이 아니라 "그 컨테이너 자신"입니다.
정리하면 범위가 이렇게 나뉩니다.
- 컨테이너 안의
localhost: 그 컨테이너 자신의 루프백 주소 - 다른 컨테이너 접근: 같은 네트워크의 서비스 이름 사용
- 호스트 머신 접근: 공개된 포트의
localhost, 또는 Docker Desktop 계열에서는host.docker.internal같은 별도 이름 사용
가장 많이 헷갈리는 주소를 한 번에 정리하기
- 브라우저에서 접속할 때:
http://localhost:8080 - web 컨테이너가 api를 부를 때:
http://api:4000 - api 컨테이너가 db에 붙을 때:
db:5432
즉, 호스트에서는 localhost, 컨테이너끼리는 서비스 이름을 쓴다는 점만 분명히 기억하면 됩니다. 다음 글(9편)에서는 이 통신 경로가 잘 유지되는지 확인하기 위한 헬스체크와 재시작 정책을 다룰 예정입니다.
💬 댓글
이 글에 대한 의견을 남겨주세요