Novita AI Research

실행 중인 Docker 컨테이너에 포트 매핑 동적으로 추가하기

실행 중인 Docker 컨테이너에 포트 매핑 동적으로 추가하기

짧은 답: 불가능합니다. Docker는 이미 실행 중인 컨테이너에 게시된 포트를 추가하는 방법을 지원하지 않습니다. 포트 매핑은 컨테이너 생성 시점에 고정되며, 이후 docker run이나 docker container update로 변경할 수 없습니다. --publish-add 플래그는 Swarm 서비스 전용이며, 독립 실행 컨테이너에는 사용할 수 없습니다.

컨테이너를 재시작할 수 없다면, 실제로 작동하는 옵션은 다음과 같습니다:

접근 방식 실제 Docker 포트 매핑을 추가하나요? Docker Desktop (macOS/Windows)에서 작동하나요? 재시작 후 유지되나요?
socat 사이드카 컨테이너 아니요 (TCP 포워드) 예 예, 사이드카가 재시작되면
리버스 프록시 (Nginx/Traefik/HAProxy) 아니요 (프록시됨) 예 예
호스트 iptables DNAT 아니요 (호스트 NAT 규칙) 아니요, 네이티브 Linux 전용 아니요, 영구 보존하지 않으면
-p 플래그로 컨테이너 재생성 예 예 예

실제로 Docker가 관리하는 게시된 포트 — docker ps와 docker port에 표시되는 포트 — 를 만드는 유일한 방법은 컨테이너를 재생성하는 것입니다. 그 외 모든 방법은 Docker의 포트 기록보다 위 또는 아래 계층에서 트래픽을 전달합니다. 재시작을 허용할 수 있는지에 따라 선택하고, 프로덕션에서 실행하기 전에 아래 주의사항을 읽어보세요.

이 글에서는 각 방법과 정확한 명령, 그리고 각각의 한계를 설명합니다.

배경: Docker 포트 매핑의 동작 방식

컨테이너 포트 매핑의 기본 원리

Docker에서 컨테이너 내부 포트와 호스트 머신의 포트 사이의 연결은 포트 매핑을 통해 이루어집니다. 일반적으로 컨테이너를 시작할 때 -p 또는 --publish 매개변수를 사용하여 포트 매핑을 지정합니다. 아래 예시를 참고하세요:

docker run -d -p 8080:80 nginx

위 명령은 호스트 머신의 8080 포트를 컨테이너 내부의 80 포트에 매핑합니다. 따라서 외부 사용자는 호스트의 8080 포트를 통해 컨테이너에서 실행 중인 웹 서비스에 접근할 수 있습니다.

Docker가 이를 허용하지 않는 이유

컨테이너가 시작된 후 Docker는 일반적으로 새 포트 매핑을 동적으로 추가하는 것을 지원하지 않습니다. 즉, 초기 포트 매핑은 컨테이너 수명 동안 고정됩니다. 포트 매핑을 더 추가해야 한다면 전통적으로 컨테이너를 중지하고 재시작해야 하는데, 이는 서비스를 중단시킬 수 있어 프로덕션 환경에서는 허용되기 어렵습니다.

네 가지 우회 방법

실행 중인 컨테이너에 포트 매핑을 동적으로 추가하려면 여러 가지 방법을 사용할 수 있습니다:

2.1 포트를 전달하는 사이드카 컨테이너 (권장)

별도의 컨테이너가 새 호스트 포트를 게시하고 공유 Docker 네트워크를 통해 원래 컨테이너로 트래픽을 전달합니다. 원래 컨테이너는 건드리지 않기 때문에 가장 안전한 옵션입니다.

먼저 중요한 정정: --network container:<name>과 -p를 함께 사용할 수 없습니다. Docker 네트워킹 문서에 따르면 container: 네트워크 모드를 사용하는 컨테이너에는 --publish, --publish-all, --expose가 지원되지 않습니다. 그런 컨테이너는 포트를 매핑할 자체 네트워크 네임스페이스가 없기 때문입니다. docker run -p 8081:81 --net container:your-container ... 명령을 사용하라고 안내하는 가이드는 모두 틀렸으며, Docker가 해당 명령을 거부합니다.

작동하는 패턴은 사용자 정의 네트워크를 사용하여 사이드카가 컨테이너 이름으로 대상에 도달하게 하는 것입니다:

# 1. 네트워크를 만들고 실행 중인 컨테이너를 연결합니다 (재시작 불필요)
docker network create app-net
docker network connect app-net your-container

# 2. 8081 포트를 게시하고 대상 컨테이너의 81 포트로 전달하는 socat 사이드카를 시작합니다
docker run -d --name port-sidecar \
  --network app-net \
  --restart unless-stopped \
  -p 8081:81 \
  alpine/socat \
  TCP-LISTEN:81,fork,reuseaddr TCP:your-container:81

docker network connect는 실행 중인 컨테이너에 적용되므로 1단계에서는 다운타임이 발생하지 않습니다. 사이드카는 자체 네임스페이스의 81 포트에서 수신하며, -p 8081:81이 이를 호스트에 게시합니다.

주의사항:

  • 이는 Docker 포트 매핑이 아니라 TCP 포워드입니다. docker port your-container에는 표시되지 않습니다.
  • alpine/socat은 TCP만 전달합니다. UDP는 UDP-LISTEN/UDP를 사용하고, 호스트 기반 라우팅이 필요한 HTTP는 Nginx, Traefik, Caddy, HAProxy를 사용하는 것이 좋습니다.
  • 재부팅 후에도 포워드를 유지하려면 (위처럼) --restart unless-stopped를 추가하세요. 그렇지 않으면 포워드가 사라집니다.
  • 추가 홉 때문에 약간의 지연 시간이 발생하고 모니터링해야 할 프로세스가 하나 더 늘어납니다.

2.2 호스트 iptables DNAT 규칙 (네이티브 Linux 전용)

네이티브 Linux 호스트에서는 호스트 포트를 컨테이너의 내부 IP로 전달하는 DNAT 규칙을 추가할 수 있습니다:

# 컨테이너 IP 가져오기
CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' your-container)

# 호스트 포트 8081을 컨테이너 포트 81로 전달
sudo iptables -t nat -A DOCKER -p tcp --dport 8081 \
  -j DNAT --to-destination "${CONTAINER_IP}:81"

이 방법은 세밀한 제어를 제공하지만 여기서 소개하는 방법 중 운영상 위험이 가장 큽니다:

  • macOS 및 Windows의 Docker Desktop에서는 작동하지 않습니다. 컨테이너는 Linux VM 안에서 실행되므로, 여러분의 머신에 적용한 iptables 규칙은 Docker의 네트워킹 경로에 영향을 주지 않습니다. 이 방법은 네이티브 Linux 전용입니다.
  • 규칙은 영구 보존되지 않습니다. 재부팅, 방화벽 리로드, nftables/iptables 전환 시 규칙이 사라집니다. 유지하려면 배포판의 방화벽 영구 보존 메커니즘을 사용하세요.
  • Docker가 DOCKER 체인을 소유합니다. Docker는 실행 중인 컨테이너의 포트 구성에서 이러한 규칙을 생성하고 관리하며, 공식 문서에서도 Docker가 만든 규칙을 수정하지 말라고 명시합니다. 사용자 정의 필터링이 필요하다면 Docker는 DOCKER-USER 체인을 사용자 규칙용 자리로 지정합니다. FORWARD에 추가된 규칙은 Docker 자체 규칙 이후에 처리되기 때문입니다.
  • 컨테이너 IP는 안정적이지 않습니다. 컨테이너가 재생성될 때마다 주소가 변경되므로, 오래된 규칙이 남아 조용히 아무 곳으로도 전달되지 않을 수 있습니다.
  • Docker의 기록을 우회합니다. 이 포트는 docker ps 또는 docker port에 표시되지 않습니다.

2.3 호스트에서 socat 직접 실행

socat을 컨테이너 대신 일반 호스트 프로세스로 실행할 수도 있습니다:

socat TCP-LISTEN:8081,fork,reuseaddr TCP:<container_ip>:81

이 방법은 컨테이너 IP가 호스트에서 라우팅 가능한 네이티브 Linux에서 작동합니다. macOS 및 Windows용 Docker Desktop에서는 컨테이너 IP가 머신에서 연결할 수 없으므로, 대신 2.1절의 사이드카를 사용하세요. 어느 쪽이든 재부팅 후에도 유지되도록 프로세스 감시자(systemd 또는 사이드카의 --restart)가 필요합니다. socat 프로세스를 셸에서 그냥 실행하면 셸이 종료될 때 함께 죽기 때문입니다.

2.4 Docker Compose로 서비스 재생성

이 방법만이 실제로 Docker가 관리하는 게시된 포트를 생성합니다. compose.yaml에 매핑을 추가하세요:

services:
  app:
    image: your-image:tag
    ports:
      - "8081:81"

그런 다음 해당 서비스만 재생성합니다:

docker compose up -d app

Compose가 컨테이너를 재생성하므로 잠시 중단이 발생합니다. 이는 라이브 변경이 아닙니다. 최신 Docker는 이전의 독립 실행형 docker-compose 바이너리가 아닌 docker compose(하위 명령)를 사용합니다. 재생성 후에도 유지되도록 상태는 named volume이나 bind mount에 보관하세요.

2.5 Docker 내부 설정 파일 편집 (비권장)

/var/lib/docker/containers/<id>/config.v2.json와 hostconfig.json을 직접 편집하여 PortBindings 항목을 추가한 다음 데몬을 재시작하라는 조언을 찾을 수 있습니다. 가끔 작동하기는 하지만 최후의 수단으로 취급하세요:

  • 이 파일들은 안정성이 보장되지 않는 내부 구현 파일 이며 지원되는 API가 아닙니다. Docker 릴리스마다 형식이 바뀔 수 있습니다.
  • 데몬은 컨테이너 상태를 메모리에 보관합니다. 실행 중인 데몬 아래에서 파일을 편집하면 변경 사항이 덮어써질 위험이 있고, 부분 편집은 컨테이너의 네트워크 상태를 메타데이터와 일치하지 않게 만들 수 있습니다.
  • 편집 전에 데몬을 중지해야 하며, 이는 호스트의 모든 컨테이너에 영향을 줍니다.
  • live-restore가 활성화되어 있으면 데몬 재시작 중에도 컨테이너는 계속 실행됩니다. 하지만 편집된 포트 매핑이 적용되지는 않습니다. live-restore는 새 게시된 포트를 추가하려면 컨테이너를 재생성해야 한다는 규칙을 바꾸지 않습니다.

데몬 상태를 손으로 편집할 지경에 이르렀다면, 올바른 -p 플래그로 컨테이너를 재생성하는 것이 더 빠르고 안전합니다.

결론

Docker는 실행 중인 컨테이너에 게시된 포트를 추가하는 것을 지원하지 않으며, 어떤 우회 방법도 이를 바꾸지 않습니다. 위 방법들이 제공하는 것은 재시작할 수 없는 컨테이너로 새 트래픽을 라우팅하는 방법입니다.

다음 순서로 선택하세요:

  1. 짧은 재시작을 허용할 수 있나요? 올바른 -p 플래그로 컨테이너를 재생성하거나, compose.yaml에 ports:를 추가하고 docker compose up -d를 실행하세요. 이 방법만이 실제 Docker 포트 매핑을 생성합니다.
  2. 재시작할 수 없나요? 2.1절의 socat 사이드카를 사용하세요. HTTP 라우팅, TLS, 헬스 체크가 필요하다면 리버스 프록시를 사용하세요.
  3. 네이티브 Linux이고 빠른 임시 포워드가 필요한가요? iptables DNAT 규칙이 작동하지만, 의도적으로 영구 보존하고 Docker가 자체 체인을 간섭할 수 있음을 감수하세요.
  4. 데몬 구성 파일의 수동 편집은 피하세요.

포트 매핑이 자주 변경된다면 그것은 보통 설계 신호입니다. 처음부터 서비스 앞에 리버스 프록시를 두고 호스트 노출 포트를 프록시가 소유하게 하여 컨테이너 수명 주기와 라우팅이 서로 독립적으로 유지되도록 하세요.

출처: Docker: Publishing ports, Docker: Container networking modes, Docker: Packet filtering and firewalls, Docker and iptables, docker container port.

GPU 인스턴스와 모델 API가 필요하다면 Novita AI를 방문하세요.

관련 게시글