컨테이너 안에서 실행 중인 서버에 외부에서 접근하려면 호스트 포트와 컨테이너 포트를 명시적으로 연결해야 한다. 많은 사람들이 Dockerfile에 EXPOSE를 적어두면 자동으로 포트가 열린다고 오해하지만, 실제 포트 바인딩은 docker run 시점에 -p 플래그로 결정된다.
EXPOSE는 컨테이너가 어떤 포트를 사용하는지 문서화하는 지시어다. 실제로 호스트에 포트를 개방하지 않는다.
Dockerfile docker run
----------- ----------
EXPOSE 3000 → 의도 선언만 (호스트 미개방)
-p 8080:3000 → 실제 바인딩
| 구분 | EXPOSE | -p |
|---|---|---|
| 위치 | Dockerfile | docker run 명령 |
| 효과 | 메타데이터 선언 | 실제 포트 바인딩 |
| 외부 접근 | 불가 | 가능 |
| 필수 여부 | 선택 | 필수 |
EXPOSE: 다른 개발자나 오케스트레이터(예: Docker Compose)가 참조할 수 있는 포트 힌트. -p 없이는 외부 트래픽이 도달하지 않는다.
-p 호스트포트:컨테이너포트 형식으로 바인딩한다.
# 호스트 8080 → 컨테이너 3000
docker run -p 8080:3000 my-app
# 호스트와 컨테이너 포트를 동일하게
docker run -p 3000:3000 my-app
호스트 머신
┌──────────────────────────────┐
│ :8080 ──────► 컨테이너 │
│ ┌──────────┐ │
│ │ :3000 │ │
│ │ Node.js │ │
│ └──────────┘ │
└──────────────────────────────┘
-p 플래그를 여러 번 사용해 복수의 포트를 동시에 매핑할 수 있다.
docker run \
-p 8080:3000 \
-p 5432:5432 \
-p 6379:6379 \
my-app
Docker Compose에서는 ports 배열로 동일하게 표현한다.
services:
app:
image: my-app
ports:
- "8080:3000"
- "5432:5432"
기본적으로 -p 8080:3000은 0.0.0.0:8080에 바인딩되어 모든 네트워크 인터페이스에서 접근 가능하다. 로컬호스트에서만 접근을 허용하려면 IP를 명시한다.
# 외부 전체 허용 (기본값)
docker run -p 8080:3000 my-app
# 로컬호스트에서만 접근
docker run -p 127.0.0.1:8080:3000 my-app
0.0.0.0: 모든 네트워크 인터페이스를 의미하는 와일드카드 주소. 프로덕션 환경에서 내부 전용 서비스는 127.0.0.1로 제한하는 것이 안전하다.
호스트 포트가 이미 사용 중이면 컨테이너 실행이 실패한다.
# 오류 예시
Error: bind: address already in use
충돌을 해결하는 방법은 두 가지다.
# 1. 다른 호스트 포트 사용
docker run -p 8081:3000 my-app
# 2. 현재 점유 중인 프로세스 확인 후 종료
lsof -i :8080
kill -9 <PID>
호스트 포트를 0으로 지정하면 Docker가 사용 가능한 포트를 자동으로 할당한다.
docker run -p 0:3000 my-app
docker port <container_id> # 할당된 포트 확인
EXPOSE는 선언, -p는 실행 — 이 두 가지 역할을 구분하면 포트 바인딩 문제의 대부분을 빠르게 진단할 수 있다.