[도커 30강] 11강. 포트 매핑과 네트워크 기본
도커를 처음 다룰 때 컨테이너는 잘 뜨는데, 브라우저에서 접속이 안 되거나 서비스 간 통신이 막혀서 당황하는 경우가 많습니다. 대부분 원인은 애플리케이션 코드가 아니라 네트워크 이해 부족입니다. 특히 컨테이너 내부 포트(expose) 와 호스트로 공개되는 포트(publish) 를 같은 개념으로 보면 디버깅이 오래 걸립니다.
이번 강의에서는 포트 매핑의 동작 원리, bridge 네트워크 기본, 컨테이너 간 통신 확인 방법, 그리고 실무에서 흔히 겪는 연결 장애를 체계적으로 정리합니다. “일단 -p 붙여본다” 수준을 넘어, 왜 연결되는지/왜 안 되는지를 설명할 수 있도록 만드는 것이 목표입니다.
핵심 개념
- 컨테이너 안의 애플리케이션은 컨테이너 네임스페이스의 네트워크에서 포트를 열고, 외부 접근은
-p 호스트포트:컨테이너포트로 별도 퍼블리시해야 가능합니다. EXPOSE는 문서화 성격이 강하며 자동 공개가 아닙니다. 실제 공개는 run/compose에서 publish 설정으로 결정됩니다.- 기본 bridge 네트워크에서는 같은 네트워크에 연결된 컨테이너끼리 서비스 이름 또는 컨테이너 이름으로 통신할 수 있습니다(사용자 정의 bridge 권장).
0.0.0.0에 바인딩하지 않고127.0.0.1에만 바인딩한 앱은 컨테이너 외부에서 접속이 안 될 수 있습니다.- 포트 충돌, 잘못된 바인딩 주소, 네트워크 분리(다른 bridge) 이 세 가지가 실무 장애의 상위 원인입니다.
기본 사용
예제 1) 포트 매핑의 기본 동작 확인
docker run --name web11 -d -p 8080:80 nginx:alpine
docker ps --filter name=web11
curl -I http://localhost:8080
설명:
nginx:alpine는 컨테이너 내부 80포트에서 HTTP를 제공합니다.-p 8080:80은 호스트 8080으로 들어온 트래픽을 컨테이너 80으로 전달합니다.docker ps의PORTS컬럼에서0.0.0.0:8080->80/tcp같은 매핑을 확인해야 합니다.
예제 2) 포트 충돌과 자동 포트 할당
# 이미 8080을 쓰는 web11이 있다고 가정
# 같은 호스트 포트를 또 쓰면 실패
docker run --name web11_conflict -d -p 8080:80 nginx:alpine || true
# 자동 할당
docker run --name web11_random -d -p 80 nginx:alpine
docker port web11_random 80
설명:
- 한 호스트 포트는 동시에 하나의 바인딩만 가능하므로 충돌 시 컨테이너 시작이 실패합니다.
-p 80처럼 호스트 포트를 생략하면 Docker가 사용 가능한 포트를 자동 할당합니다.- 로컬 병렬 테스트에서는 자동 할당이 편하지만, 팀 환경에서는 문서화된 고정 포트가 디버깅에 유리합니다.
예제 3) 사용자 정의 bridge 네트워크에서 서비스 간 통신
# 사용자 정의 네트워크 생성
docker network create app11_net
# API 역할 컨테이너 실행
docker run --name api11 --network app11_net -d hashicorp/http-echo \
-text="hello from api11" -listen=:5678
# 클라이언트 컨테이너에서 이름 기반 호출
docker run --rm --network app11_net curlimages/curl:8.6.0 \
-s http://api11:5678
설명:
- 사용자 정의 bridge에서는 Docker 내장 DNS가 컨테이너 이름(
api11)을 해석합니다. - 이 통신은 호스트로 포트를 공개하지 않아도 같은 네트워크 내부에서 가능합니다.
- 내부 통신과 외부 공개를 분리하면 보안/운영 설계가 훨씬 명확해집니다.
예제 4) 애플리케이션 바인딩 주소 확인
# Python HTTP 서버를 127.0.0.1에만 바인딩
docker run --name py11_bad -d -p 8091:8000 python:3.12-alpine \
sh -c "python -m http.server 8000 --bind 127.0.0.1"
# 호스트 접근 시 실패 가능
curl -I http://localhost:8091 || true
# 0.0.0.0으로 바꿔 재실행
docker rm -f py11_bad
docker run --name py11_good -d -p 8091:8000 python:3.12-alpine \
sh -c "python -m http.server 8000 --bind 0.0.0.0"
curl -I http://localhost:8091
설명:
- 컨테이너 내부 루프백(127.0.0.1)은 컨테이너 자신만 접근 가능한 인터페이스입니다.
- 외부(호스트→컨테이너) 접근을 허용하려면 보통
0.0.0.0바인딩이 필요합니다. - “포트 매핑은 했는데 접속이 안 된다”의 가장 흔한 원인 중 하나입니다.
자주 하는 실수
실수 1) EXPOSE를 넣었으니 외부 접속이 될 거라고 믿음
- 원인: Dockerfile의
EXPOSE 8080을 publish와 동일하게 오해. - 해결: EXPOSE는 문서적 힌트일 뿐입니다. 실제 공개는
docker run -p, compose의ports:로 설정합니다.
실수 2) 컨테이너 포트와 호스트 포트를 바꿔 적음
- 원인:
-p 80:8080vs-p 8080:80의미를 혼동. - 해결: 형식을 “들어오는 쪽(호스트):앱이 듣는 쪽(컨테이너)”로 외워 고정합니다. 앱이 8080에서 뜨면 오른쪽은 반드시 8080이어야 합니다.
실수 3) 서비스 간 통신에 localhost를 사용
- 원인: 컨테이너 내부의
localhost가 자기 자신을 가리킨다는 사실을 놓침. - 해결: 같은 Docker 네트워크에서는
http://서비스명:포트형태를 사용합니다. 예:http://postgres:5432.
실수 4) 기본 bridge와 사용자 정의 bridge를 섞어 씀
- 원인: 컨테이너마다 실행 명령이 달라 서로 다른 네트워크에 붙음.
- 해결: 프로젝트 단위로 명시적 네트워크를 만들고, compose에서는
networks:를 고정하여 누락을 방지합니다.
실무 패턴
실무에서는 네트워크를 “외부 진입점”과 “내부 서비스망”으로 분리해서 설계합니다. 예를 들어 web(Nginx/API Gateway)만 호스트 포트를 공개하고, app, redis, postgres는 내부 네트워크에서만 통신하도록 두는 방식입니다. 이렇게 하면 공격 표면이 줄고, 포트 정책도 단순해집니다.
로컬 개발에서도 이 패턴을 미리 적용하면 운영 전환이 훨씬 부드럽습니다. 개발자는 호스트에서 localhost:8080으로 web만 접근하고, web은 내부에서 app으로 프록시합니다. app은 다시 DB/캐시와 내부 통신합니다. 즉, 외부 노출이 필요한 계층만 공개하는 습관을 들이면 배포 시 보안 설정이 갑자기 복잡해지지 않습니다.
팀 협업에서는 포트 규칙 표준화가 중요합니다. 예를 들어 api=18080, admin=18081, grafana=13000처럼 로컬 포트 체계를 문서화하면 “내 컴퓨터에서는 8080이 이미 점유됨” 같은 충돌을 줄일 수 있습니다. 그리고 CI/테스트에서는 랜덤 포트를 쓰되 결과를 로그에 남겨 병렬 실행 충돌을 피합니다.
문제 해결 루틴도 고정해두면 좋습니다. 연결이 안 될 때는 다음 순서로 확인합니다. (1) 컨테이너가 실제 실행 중인지, (2) 앱 프로세스가 올바른 포트에서 LISTEN 중인지, (3) publish 매핑이 맞는지, (4) 앱 바인딩 주소가 0.0.0.0인지, (5) 통신 대상이 같은 네트워크인지. 이 순서를 팀 공통 체크리스트로 두면 불필요한 감정 소모를 줄일 수 있습니다.
보안 관점에서는 “열어야 하는 포트만 연다”가 핵심입니다. 특히 데이터베이스 포트를 습관적으로 호스트에 publish하는 것은 피해야 합니다. 로컬에서 꼭 필요할 때만 제한적으로 열고, 운영에서는 bastion/프록시/사설망 접근 정책으로 통제하는 쪽이 안전합니다. 또한 docker run -p 127.0.0.1:5432:5432처럼 바인딩 IP를 명시해 로컬 루프백으로만 노출하는 패턴도 유용합니다.
마지막으로 compose에서 네트워크와 포트를 선언적으로 관리하세요. 개인 실행 명령에 의존하면 환경마다 결과가 달라집니다. docker compose config로 최종 렌더링 결과를 점검하고, 변경 시에는 반드시 “어떤 포트가 새로 노출되는지”를 리뷰 항목으로 넣는 것을 추천합니다.
오늘의 결론
한 줄 요약: 포트 매핑은 외부 공개 정책이고, Docker 네트워크는 내부 통신 정책이다. 이 둘을 분리해서 설계하면 장애와 보안을 동시에 잡을 수 있다.
포트가 열린다는 사실 자체보다, 왜 그 포트를 열었는지 설명할 수 있어야 운영 가능한 시스템이 됩니다.
연습문제
- Nginx 컨테이너를
-p 8082:80으로 실행하고,docker port,curl로 매핑 상태를 검증해보세요. 이후-p 80자동 할당 방식과 차이를 정리하세요. docker network create lesson11_net을 만든 뒤, API 컨테이너와 curl 컨테이너를 같은 네트워크에 붙여 이름 기반 호출(http://api:포트)을 성공시켜 보세요.- Python HTTP 서버를 한 번은
127.0.0.1, 한 번은0.0.0.0으로 바인딩해 실행하고, 호스트 접근 가능 여부가 왜 달라지는지 네트워크 관점에서 설명해보세요.
이전 강의 정답
10강 연습문제 해설:
- 볼륨 없이 컨테이너 writable layer에 파일을 저장하면 컨테이너 삭제/재생성 시 데이터가 사라집니다. 반면 named volume을
/data에 연결하면 컨테이너를 교체해도 같은 볼륨을 다시 붙이는 순간 데이터가 유지됩니다. - 바인드 마운트는 호스트 파일을 즉시 반영하므로 개발 루프가 빠릅니다.
:ro를 주면 컨테이너에서 쓰기 불가라 안전성이 올라가고,:ro를 제거하면 양방향 수정이 가능하지만 실수로 호스트 파일이 바뀔 위험도 커집니다. - Postgres에 볼륨을 연결한 상태에서 테이블/레코드를 만든 뒤 컨테이너 재생성 후 조회가 성공하면 영속성이 검증됩니다. 팀 체크리스트 예시는 다음과 같습니다: (1) DB 서비스에 named volume 강제, (2) 배포 전/후 샘플 조회, (3) 주기적 백업, (4) 복원 리허설, (5) 볼륨 정리 명령 승인 절차.
실습 환경/재현 정보
- OS: macOS 15+ 또는 Ubuntu 22.04+
- Docker 버전: Docker Desktop/Engine 25.x 이상
- 사용 이미지:
nginx:alpine,hashicorp/http-echo,curlimages/curl,python:3.12-alpine - 실행 순서:
- Nginx 포트 매핑 확인 (
-p host:container) - 포트 충돌 및 자동 할당 비교
- 사용자 정의 bridge 네트워크 생성
- 서비스 이름 기반 내부 통신 검증
- 바인딩 주소(127.0.0.1/0.0.0.0) 차이 재현
- Nginx 포트 매핑 확인 (
- 재현 체크:
docker ps에서 기대한 포트 매핑이 보이는가?- 같은 네트워크에서 서비스 이름으로 통신되는가?
- 앱 바인딩 주소가 외부 접근 요구사항과 일치하는가?
- 불필요한 포트(특히 DB 포트)를 외부에 노출하지 않았는가?