Theme:

docker run -p 8080:80이라고 쓰면 외부에서 8080으로 접근할 수 있게 됩니다. 간단해 보이지만, 이 한 줄 뒤에서는 iptables 규칙 추가, docker-proxy 프로세스 생성 등 여러 일이 벌어집니다.

포트 매핑의 기본

BASH
# 기본 형태: 호스트포트:컨테이너포트
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 규칙을 추가합니다.

패킷 흐름

PLAINTEXT
외부 클라이언트 (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 규칙 확인

BASH
# 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를 직접 조작하므로, ufwfirewalld 같은 방화벽 도구의 규칙을 우회할 수 있습니다.

BASH
# 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 프로세스도 함께 생성합니다.

BASH
# 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가 사용됩니다.

  1. 컨테이너에서 호스트의 매핑된 포트로 접근할 때 (hairpin NAT)
  2. IPv6 트래픽 (iptables IPv6 규칙이 없을 때)
  3. iptables가 비활성화된 환경

docker-proxy 비활성화

docker-proxy는 각 포트 매핑마다 프로세스를 생성하므로 리소스를 소비합니다. 필요 없다면 비활성화할 수 있습니다.

JSON
// /etc/docker/daemon.json
{
  "userland-proxy": false
}
BASH
sudo systemctl restart docker

비활성화하면 순수하게 iptables만으로 포트 포워딩을 처리합니다. hairpin NAT 문제가 발생할 수 있으니 주의하세요.

EXPOSE vs -p

DOCKERFILE
# Dockerfile에서 EXPOSE — 문서화 목적
EXPOSE 80
EXPOSE 443

EXPOSE실제로 포트를 열지 않습니다. 어떤 포트를 사용하는지 알려주는 문서 역할입니다.

BASH
# -p로만 실제 포트 매핑이 됨
docker run -d -p 8080:80 myapp

# -P: EXPOSE된 모든 포트를 자동으로 랜덤 포트에 매핑
docker run -d -P myapp

리버스 프록시 패턴

프로덕션에서는 직접 포트 매핑보다 리버스 프록시를 앞에 두는 것이 일반적입니다.

Nginx 리버스 프록시

YAML
# 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
# 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를 감시하여 컨테이너의 라벨을 읽고 자동으로 라우팅을 구성합니다.

YAML
# 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

특성NginxTraefik
설정 방식설정 파일 수동 관리Docker 라벨 자동
리로드설정 변경 후 reload 필요자동 감지
SSL 인증서certbot 등 별도 설정내장 ACME
정적 파일매우 우수미지원
성능매우 높음높음
학습 곡선높음낮음

실무 팁

1. 불필요한 포트 노출 피하기

YAML
# 나쁜 예: 모든 서비스에 ports 사용
services:
  db:
    ports:
      - "5432:5432"  # DB가 외부에 노출!

# 좋은 예: 내부 서비스는 expose만
services:
  db:
    expose:
      - "5432"  # 같은 네트워크의 컨테이너에서만 접근 가능

2. 특정 인터페이스에만 바인드

BASH
# 모든 인터페이스 (기본값) — 외부에서도 접근 가능
docker run -p 8080:80 nginx

# localhost만 — 같은 머신에서만 접근
docker run -p 127.0.0.1:8080:80 nginx

3. 컨테이너 포트 확인

BASH
# 매핑된 포트 확인
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를 사용합니다.
댓글 로딩 중...