컨테이너 포트 매핑과 프록시 — 외부 요청이 컨테이너에 도달하는 과정
docker run -p 8080:80이라고 쓰면 외부에서 8080으로 접근할 수 있게 됩니다. 간단해 보이지만, 이 한 줄 뒤에서는 iptables 규칙 추가, docker-proxy 프로세스 생성 등 여러 일이 벌어집니다.
포트 매핑의 기본
# 기본 형태: 호스트포트:컨테이너포트
docker run -d -p 8080:80 nginx
# 여러 포트 매핑
docker run -d -p 8080:80 -p 8443:443 nginx
# 호스트 포트 자동 할당
docker run -d -p 80 nginx
# docker port 명령으로 할당된 포트 확인
docker port <container-id>
# 80/tcp -> 0.0.0.0:32768
# 특정 인터페이스에만 바인드
docker run -d -p 127.0.0.1:8080:80 nginx # localhost만
docker run -d -p 192.168.1.10:8080:80 nginx # 특정 IP만
# UDP 포트 매핑
docker run -d -p 5353:53/udp dns-server
# 포트 범위 매핑
docker run -d -p 8000-8010:8000-8010 myapp
내부 동작: iptables
Docker가 포트 매핑을 설정하면, 리눅스의 iptables에 NAT 규칙을 추가합니다.
패킷 흐름
외부 클라이언트 (1.2.3.4:54321)
│
↓ 목적지: 호스트:8080
┌── iptables (PREROUTING) ────────┐
│ DNAT: 172.17.0.2:80 으로 변환 │
└─────────────┬───────────────────┘
│
↓ 목적지: 172.17.0.2:80
┌── iptables (FORWARD) ──────────┐
│ docker0 → veth 으로 포워딩 │
└─────────────┬──────────────────┘
│
↓
┌── 컨테이너 (172.17.0.2:80) ───┐
│ nginx 응답 │
└────────────────────────────────┘
실제 iptables 규칙 확인
# NAT 테이블 확인
sudo iptables -t nat -L -n --line-numbers
# DOCKER 체인에 매핑 규칙이 추가됨
# Chain DOCKER (2 references)
# num target prot opt source destination
# 1 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
# FORWARD 체인
sudo iptables -L DOCKER -n
# Chain DOCKER (1 references)
# ACCEPT tcp -- 0.0.0.0/0 172.17.0.2 tcp dpt:80
Docker는 다음 iptables 체인을 관리합니다.
DOCKER: 포트 매핑 DNAT 규칙DOCKER-ISOLATION-STAGE-1,DOCKER-ISOLATION-STAGE-2: 네트워크 간 격리DOCKER-USER: 사용자 정의 규칙 (Docker가 건드리지 않음)
주의: Docker와 방화벽
Docker는 iptables를 직접 조작하므로, ufw나 firewalld 같은 방화벽 도구의 규칙을 우회할 수 있습니다.
# UFW에서 8080을 차단했는데도 Docker 포트 매핑은 동작할 수 있음!
ufw deny 8080 # 이것만으로는 Docker 포트를 차단할 수 없음
# Docker 포트를 제어하려면 DOCKER-USER 체인 사용
sudo iptables -I DOCKER-USER -p tcp --dport 80 -j DROP
sudo iptables -I DOCKER-USER -s 10.0.0.0/8 -p tcp --dport 80 -j ACCEPT
docker-proxy
-p 옵션을 사용하면 Docker는 docker-proxy라는 userland 프로세스도 함께 생성합니다.
# docker-proxy 프로세스 확인
ps aux | grep docker-proxy
# docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 \
# -container-ip 172.17.0.2 -container-port 80
docker-proxy가 필요한 이유
주로 iptables로 모든 트래픽을 처리할 수 있지만, 다음 경우에 docker-proxy가 사용됩니다.
- 컨테이너에서 호스트의 매핑된 포트로 접근할 때 (hairpin NAT)
- IPv6 트래픽 (iptables IPv6 규칙이 없을 때)
- iptables가 비활성화된 환경
docker-proxy 비활성화
docker-proxy는 각 포트 매핑마다 프로세스를 생성하므로 리소스를 소비합니다. 필요 없다면 비활성화할 수 있습니다.
// /etc/docker/daemon.json
{
"userland-proxy": false
}
sudo systemctl restart docker
비활성화하면 순수하게 iptables만으로 포트 포워딩을 처리합니다. hairpin NAT 문제가 발생할 수 있으니 주의하세요.
EXPOSE vs -p
# Dockerfile에서 EXPOSE — 문서화 목적
EXPOSE 80
EXPOSE 443
EXPOSE는 실제로 포트를 열지 않습니다. 어떤 포트를 사용하는지 알려주는 문서 역할입니다.
# -p로만 실제 포트 매핑이 됨
docker run -d -p 8080:80 myapp
# -P: EXPOSE된 모든 포트를 자동으로 랜덤 포트에 매핑
docker run -d -P myapp
리버스 프록시 패턴
프로덕션에서는 직접 포트 매핑보다 리버스 프록시를 앞에 두는 것이 일반적입니다.
Nginx 리버스 프록시
# compose.yaml
services:
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs:/etc/nginx/certs:ro
networks:
- frontend
api:
image: myapi:latest
# 포트를 외부에 노출하지 않음!
expose:
- "8080"
networks:
- frontend
- backend
db:
image: postgres:16-alpine
networks:
- backend
networks:
frontend:
backend:
# nginx.conf
upstream api_backend {
server api:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
이 구조에서 api 서비스는 외부에 직접 노출되지 않고, Nginx를 통해서만 접근 가능합니다.
Traefik — 자동 구성 프록시
Traefik은 Docker API를 감시하여 컨테이너의 라벨을 읽고 자동으로 라우팅을 구성합니다.
# compose.yaml
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json
api:
image: myapi:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.services.api.loadbalancer.server.port=8080"
web:
image: myweb:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`www.example.com`)"
- "traefik.http.routers.web.entrypoints=websecure"
- "traefik.http.routers.web.tls.certresolver=letsencrypt"
Traefik의 장점:
- 컨테이너 추가/제거 시 자동으로 라우팅 갱신
- Let's Encrypt 인증서 자동 발급/갱신
- Docker 라벨만으로 설정 (별도 설정 파일 불필요)
- 대시보드로 라우팅 현황 시각적 확인
Nginx vs Traefik
| 특성 | Nginx | Traefik |
|---|---|---|
| 설정 방식 | 설정 파일 수동 관리 | Docker 라벨 자동 |
| 리로드 | 설정 변경 후 reload 필요 | 자동 감지 |
| SSL 인증서 | certbot 등 별도 설정 | 내장 ACME |
| 정적 파일 | 매우 우수 | 미지원 |
| 성능 | 매우 높음 | 높음 |
| 학습 곡선 | 높음 | 낮음 |
실무 팁
1. 불필요한 포트 노출 피하기
# 나쁜 예: 모든 서비스에 ports 사용
services:
db:
ports:
- "5432:5432" # DB가 외부에 노출!
# 좋은 예: 내부 서비스는 expose만
services:
db:
expose:
- "5432" # 같은 네트워크의 컨테이너에서만 접근 가능
2. 특정 인터페이스에만 바인드
# 모든 인터페이스 (기본값) — 외부에서도 접근 가능
docker run -p 8080:80 nginx
# localhost만 — 같은 머신에서만 접근
docker run -p 127.0.0.1:8080:80 nginx
3. 컨테이너 포트 확인
# 매핑된 포트 확인
docker port mycontainer
# 컨테이너 내부에서 리스닝 중인 포트
docker exec mycontainer ss -tlnp
# 또는
docker exec mycontainer netstat -tlnp
정리
- Docker 포트 매핑은 내부적으로 iptables NAT 규칙과 docker-proxy 프로세스로 구현됩니다.
EXPOSE는 문서화 목적이며, 실제 포트 매핑은-p플래그로만 됩니다.- 프로덕션에서는 서비스를 직접 노출하기보다 리버스 프록시(Nginx, Traefik)를 앞에 두는 것이 보안과 관리 면에서 유리합니다.
- Docker가 iptables를 직접 조작하므로 방화벽 설정 시
DOCKER-USER체인을 활용해야 합니다. - 보안을 위해 내부 서비스는
expose만 사용하고, 외부에 노출이 필요한 서비스만ports를 사용합니다.
댓글 로딩 중...