SSD Nodes Learn 🎉 VPS $4.99/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-05

Gluetun 컨테이너 네트워크 통신 및 포트 설정 방법

Gluetun 네트워크에 참여한 컨테이너는 자체 인터페이스가 없어 포트 게시 시 오류가 발생합니다. 포트를 Gluetun 서비스로 옮겨 매핑하고, TUN 인터페이스를 통해 통신할 서브넷만 허용하는 올바른 설정 방법을 확인하십시오.

컨테이너가 gluetun 네트워크에 참여하면 발생하는 일

network_mode: service:gluetun를 설정한 컨테이너는 자체 네트워크 인터페이스를 가지지 않습니다. 이 컨테이너는 gluetun의 네트워크 네임스페이스에 참여하게 되며, 따라서 포트 게시 및 방화벽 규칙은 해당 컨테이너의 속성이 아니라 gluetun 서비스의 속성이 됩니다. 아래의 모든 답변은 이 사실에서 비롯됩니다.

네트워크 네임스페이스는 커널이 관리하는 네트워크 스택의 독립적인 복사본으로, 고유한 인터페이스, 라우팅 테이블, 방화벽 규칙 및 리스닝 소켓을 포함합니다. Docker는 기본적으로 각 컨테이너에 하나씩 할당합니다. network_mode: service:gluetun를 작성하면 Docker는 해당 단계를 건너뛰고 새 컨테이너를 gluetun이 이미 소유한 네임스페이스 안에 배치합니다. 컨테이너는 자신의 파일 시스템과 /etc/hosts 파일을 유지하며, 이 두 번째 파일은 나중에 중요하게 작용합니다.

이를 직접 확인할 수 있습니다.

docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrent

이 명령은 container:과 gluetun 컨테이너 ID를 출력합니다. 일반적인 컨테이너라면 bridge을 출력했을 것입니다. 이 가이드는 gluetun을 사용하여 Docker 트래픽을 VPN으로 라우팅하기에서 이어집니다. 터널은 정상적으로 작동하지만, 이제 컨테이너와 통신할 수 없는 상태가 됩니다.

포트는 애플리케이션이 아닌 gluetun에 게시하십시오

서비스에 ports: 블록을 남겨두고 network_mode을 설정하면 Docker는 컨테이너 생성을 거부합니다:

Error response from daemon: conflicting options: port publishing and the container type network mode

이유는 간단합니다. 포트를 게시한다는 것은 호스트 포트를 컨테이너의 네트워크 네임스페이스로 전달하는 NAT(네트워크 주소 변환) 규칙을 추가하는 것인데, 이 컨테이너에는 해당 네임스페이스가 없기 때문입니다. 매핑을 gluetun 서비스로 옮기십시오. 애플리케이션은 여전히 공유 네임스페이스 내의 해당 포트에서 수신 대기 중이므로 포트 번호는 변경되지 않습니다.

services:
  gluetun:
    ports:
      - "8080:8080/tcp"   # qBittorrent web UI

  qbittorrent:
    network_mode: "service:gluetun"
    # no ports: block here

종속 서비스의 expose: 블록도 마찬가지로 무의미하며, 그곳의 networks: 블록은 치명적인 오류를 발생시킵니다. Compose는 해당 서비스가 상호 배타적인 network_modenetworks를 선언했다고 보고하며 파일 로드를 완전히 거부합니다.

한 가지 결과가 나중에 문제가 됩니다. 네임스페이스 내의 모든 컨테이너는 단일 포트 공간을 공유하므로, 기본값이 모두 8080인 두 애플리케이션이 충돌하게 되며 나중에 시작되는 애플리케이션은 주소 이미 사용 중(address already in use) 오류로 실패합니다. 애플리케이션 자체 설정에서 하나를 변경하십시오. 예를 들어 LinuxServer qBittorrent 이미지의 WEBUI_PORT 변수를 변경한 다음, gluetun에서 새 번호를 게시하십시오.

gluetun 뒤에 있는 컨테이너들은 서로 어떻게 통신합니까?

네임스페이스 내부에서 이들은 이미 루프백 인터페이스를 공유합니다. gluetun 뒤에 있는 컨테이너는 Docker 네트워크를 거치지 않고 127.0.0.1:<port>에서 형제 컨테이너에 도달합니다.

네임스페이스 외부에서 볼 때, 해당 컨테이너는 이름이 없습니다. Docker의 내장 DNS는 서비스 이름을 사용자 정의 네트워크상의 해당 서비스 주소로 해석하는데, 이 컨테이너는 어떤 네트워크에도 주소를 가지고 있지 않습니다. 따라서 Sonarr와 같은 일반 컨테이너는 http://qbittorrent:8080에서 토렌트 클라이언트에 도달하지 못합니다. 소켓이 gluetun의 네임스페이스 내, 즉 gluetun의 주소에서 리스닝 중이므로 http://gluetun:8080을 통해 도달해야 합니다. 이는 Docker Compose 네트워크와 서비스 이름의 작동 방식을 알고 있으며 일반적인 이름 지정 규칙이 적용될 것이라 예상하는 사용자들을 당황하게 만듭니다. 두 컨테이너가 같은 Compose 네트워크에 있다면 호스트에 포트를 공개하지 않아도 이 방식은 정상적으로 작동합니다.

다른 문제를 디버깅하기 전에 DNS를 먼저 확인하십시오. gluetun은 자체 리졸버를 실행하며 자신의 컨테이너 내에서 /etc/resolv.conf를 다시 작성합니다. 하지만 /etc/resolv.conf은 컨테이너별 파일이므로, gluetun이 작성한 파일은 애플리케이션이 읽는 파일과 다릅니다.

docker exec qbittorrent cat /etc/resolv.conf

Docker 호스트에서 실행 중인 서비스에 어떻게 접근합니까?

host.docker.internal을 사용하십시오. 두 가지가 서로 다르게 설정되어야 하므로, 두 곳에서 각각 설정을 변경해야 합니다.

먼저 이름 설정입니다. /etc/hosts는 컨테이너별로 적용되므로, extra_hosts 항목은 gluetun이 아닌 애플리케이션 컨테이너에 작성해야 합니다.

  prowlarr:
    network_mode: "service:gluetun"
    extra_hosts:
      - "host.docker.internal:host-gateway"

host-gateway는 Docker가 호스트 자체의 내부 주소로 치환하는 특수 값입니다. 일반적인 Linux Docker 설치 환경에서는 docker0 브리지의 주소이며, 보통 172.17.0.1입니다. VPS에서 ip -4 addr show docker0 명령어로 본인의 주소를 확인하십시오. Docker Desktop은 이 이름을 자체적으로 해석하므로, 노트북 환경에서 작성된 가이드들은 extra_hosts 줄을 생략하는 경우가 많습니다. 이 때문에 서버에서는 동일한 파일이 작동하지 않게 됩니다.

다음은 경로 설정입니다. 이름만 추가한다고 해서 컨테이너가 어떤 주소를 사용할지 알 수 있는 것은 아닙니다. 패킷은 여전히 gluetun의 기본 경로인 터널을 통해 나가려 할 것이고, gluetun의 방화벽은 이를 차단합니다. 이 경우 연결이 응답 없이 대기하다가 시간 초과(timeout)되는 증상이 나타납니다. 연결 거부(refused)는 패킷이 도착하여 거절 응답을 받은 경우이고, 시간 초과는 패킷이 아예 도착하지 못한 경우입니다.

  gluetun:
    environment:
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32

그다음, 호스트 서비스가 해당 주소에서 실제로 수신 대기 중인지 확인하십시오. 127.0.0.1에만 바인딩된 PostgreSQL 서버는 터널 여부와 관계없이 어떤 컨테이너에서도 접근할 수 없습니다. 네임스페이스 내부의 127.0.0.1은 해당 네임스페이스 자신의 루프백이기 때문입니다. 대신 172.17.0.1에 바인딩하십시오. 이렇게 하면 공용 인터페이스를 노출하지 않으면서 컨테이너로부터의 연결을 허용할 수 있습니다. 호스트에서 ss -lntp | grep 5432 명령어로 이를 확인하십시오.

FIREWALL_OUTBOUND_SUBNETS가 실제로 변경하는 내용

gluetun 문서에서는 이 설정을 gluetun과 해당 네트워크 스택을 공유하는 컨테이너가 접근할 수 있는 쉼표로 구분된 서브넷 목록으로 설명하며, 방화벽 및 라우팅 변경이 수반된다고 명시합니다. 두 가지 측면 모두 중요합니다. gluetun은 나열된 각 서브넷에 대해 Docker 브리지 게이트웨이를 통하는 경로를 추가하므로, 해당 주소로 향하는 패킷은 터널이 아닌 eth0을 통해 나갑니다. 또한 gluetun은 VPN 서버로 향하지 않는 아웃바운드 트래픽을 기본적으로 차단하기 때문에, 이 설정을 통해 해당 서브넷에 대한 방화벽을 개방합니다.

값을 작성할 때는 쉼표 뒤에 공백을 넣지 마십시오.

      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32

간과하기 쉬운 두 가지 특성이 있습니다. 이 설정은 네임스페이스 수준에서 적용되므로, 의도했던 컨테이너뿐만 아니라 gluetun 뒤에 있는 모든 컨테이너에 영향을 미칩니다. 또한 이 설정은 아웃바운드에만 적용됩니다. 즉, 컨테이너가 시작하는 연결만을 제어합니다. 게시된 포트로 들어오는 연결은 다른 경로를 통하므로 여기에 입력할 필요가 없습니다.

Tailscale 피어에서 웹 UI 접속하기

Tailscale은 모든 머신에 100.64.0.0/10 대역의 주소를 할당하며, 이는 캐리어급 NAT(CGNAT)를 위해 예약된 범위입니다. 이 대역을 통한 양방향 통신을 위해서는 각각 다른 설정이 필요합니다.

인바운드 연결은 간단합니다. gluetun에서 8080:8080를 게시(publish)하면 호스트의 모든 주소에서 해당 포트가 바인딩됩니다. 호스트의 tailscale0 인터페이스도 그중 하나이므로, 피어는 http://<machine-name>:8080로 접속하여 컨테이너에 도달할 수 있습니다. 이 경로에서 gluetun은 아무런 역할을 하지 않습니다. Docker의 NAT 규칙이 네임스페이스 외부인 호스트에서 동작하기 때문입니다.

UI를 tailnet을 통해서만 접속할 수 있게 하려면, 게시된 포트를 모든 주소가 아닌 호스트의 Tailscale 주소에만 바인딩하십시오.

    ports:
      - "100.101.102.103:8080:8080/tcp"

호스트에서 tailscale ip -4 명령을 사용하여 해당 주소를 찾을 수 있습니다. 이 방식은 포트가 공용 인터페이스에서 아예 열리지 않으므로 방화벽 규칙보다 더 강력한 제어 수단이 됩니다. 또한 Docker가 ufw를 우회하여 포트를 게시하는 문제도 피할 수 있습니다.

아웃바운드 연결은 FIREWALL_OUTBOUND_SUBNETS가 다시 문제가 되는 지점입니다. 컨테이너가 피어를 호출해야 한다면 해당 피어의 주소를 추가하십시오. 이때 전체 /10 대역을 허용하기보다는 피어별로 /32를 사용하는 것이 좋습니다. 컨테이너는 호스트의 리졸버를 사용하지 않으므로 MagicDNS 이름은 컨테이너 내부에서 해석되지 않습니다. 따라서 숫자 형태의 100.x 주소를 사용하거나 extra_hosts 라인으로 고정하십시오. 자체 Tailscale 컨트롤 서버인 Headscale을 운영할 때도 동일하게 적용됩니다.

일반적인 구성을 위한 완성된 compose 파일

VPN 뒤에서 동작하는 다운로드 클라이언트, tailnet에서만 응답하는 두 개의 웹 UI, 그리고 호스트에서 실행 중인 PostgreSQL 데이터베이스를 읽는 컨테이너 하나로 구성됩니다.

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - "127.0.0.1:8000:8000/tcp"        # gluetun control server, host only
      - "100.101.102.103:8080:8080/tcp"  # qBittorrent UI, tailnet only
      - "100.101.102.103:9696:9696/tcp"  # Prowlarr UI, tailnet only
    volumes:
      - ./gluetun:/gluetun
    environment:
      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
      - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
      - SERVER_CITIES=Amsterdam
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
      - TZ=Europe/Amsterdam
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent:/config
      - /srv/downloads:/downloads
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    network_mode: "service:gluetun"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - PROWLARR__POSTGRES__HOST=host.docker.internal
      - PROWLARR__POSTGRES__PORT=5432
      - PROWLARR__POSTGRES__USER=prowlarr
      - PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
      - PROWLARR__POSTGRES__MAINDB=prowlarr-main
      - PROWLARR__POSTGRES__LOGDB=prowlarr-log
    volumes:
      - ./prowlarr:/config
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

파일의 제품명보다는 패턴을 확인하십시오. 두 UI 모두 gluetun에 게시되고 호스트의 tailnet 주소에 바인딩되어 있으므로, Tailscale 외의 다른 곳에서는 응답하지 않습니다. Prowlarr만이 extra_hosts 줄을 포함하는데, 이는 Prowlarr가 host.docker.internal를 해석하는 컨테이너이기 때문입니다. FIREWALL_OUTBOUND_SUBNETS은 두 개의 단일 주소를 지정합니다. 하나는 Prowlarr가 데이터베이스 연결을 열 수 있도록 하는 호스트의 Docker 브리지 주소이고, 다른 하나는 tailnet 피어 주소입니다.

PostgreSQL 서버는 의도적으로 이 파일에서 제외되었습니다. 이 서버는 172.17.0.1:5432에서 수신 대기하는 일반 시스템 서비스로 VPS에서 실행됩니다. 이는 Docker Compose의 arr 스택과 동일한 계층 구조이며, 데이터베이스만 Docker 외부로 이동한 형태입니다.

WireGuard 개인 키는 compose 파일에 포함하지 마십시오. ${WIREGUARD_PRIVATE_KEY}은 옆에 있는 .env 파일에서 값을 읽어오며, 이 패턴은 Docker Compose를 위한 env 파일 및 보안 설정에서 다룹니다. condition: service_healthy 절은 gluetun 이미지가 기본으로 제공하는 healthcheck를 사용하므로, 터널이 연결되었다고 보고하기 전까지는 아무것도 시작되지 않습니다. Compose healthcheck에서 일반적인 형식을 설명합니다.

tailnet이 아닌 모든 주소로 게시하기

0.0.0.0에서 주소 접두사와 포트 바인딩을 제거하면 VPS 공인 IP를 포함한 모든 주소로 게시됩니다. 이 작업은 반드시 직접 제어하는 방화벽 뒤에서만 수행해야 하며, 먼저 위의 ufw 관련 주의 사항을 읽어보십시오.

    ports:
      - "8080:8080/tcp"

터널이 트래픽을 정상적으로 전달하는지 확인

네임스페이스 내부에서 한 번, 호스트에서 한 번, 동일한 요청을 두 번 실행하여 결과를 비교합니다.

docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.org

첫 번째 요청은 VPN 제공업체의 출구 IP 주소를 출력해야 합니다. 두 번째 요청은 VPS의 IP 주소를 출력해야 합니다. 두 주소가 일치한다면 컨테이너의 트래픽이 터널을 통하지 않는 상태입니다. 이 문제가 해결되기 전까지는 이 가이드의 다른 모든 수정 사항은 의미가 없습니다.

라우팅 테이블을 통해 터널을 통하는 트래픽과 그렇지 않은 트래픽을 확인할 수 있습니다.

docker run --rm --network=container:gluetun alpine:3.22 ip route show

기본 경로는 터널 인터페이스인 tun0를 가리켜야 합니다. 그 아래에는 FIREWALL_OUTBOUND_SUBNETS의 각 항목마다 하나씩, Docker 브리지 게이트웨이를 가리키는 경로가 보여야 합니다. eth0를 통해 나가는 다른 모든 경로는 VPN을 우회하는 트래픽입니다.

Gluetun의 제어 서버는 /v1/publicip/ip의 8000번 포트에서 동일한 공인 IP를 보고합니다. 최신 버전에서는 제어 서버 경로에 대한 인증을 설정해야 하므로, 이를 사용하기 전에 먼저 설정을 완료하십시오.

잘못된 서브넷 설정으로 발생하는 유출

FIREWALL_OUTBOUND_SUBNETS은 의도적으로 방화벽에 뚫는 구멍이므로, 구멍의 크기가 곧 위험의 크기입니다. 이 구멍이 너무 커지는 네 가지 경우는 다음과 같습니다.

  • 0.0.0.0/0은 모든 트래픽을 터널 외부로 보냅니다. 위에서 언급한 두 가지 IP 확인 절차를 거치면 첫 실행 시 동일한 주소가 반환되므로 이 문제를 즉시 발견할 수 있습니다.
  • 대상보다 넓은 범위의 설정입니다. 10.0.1.7에 있는 단일 장비에 접근하려고 10.0.0.0/8 범위를 열면, 해당 범위 내에서 토렌트 피어가 광고할 수 있는 모든 주소까지 함께 열리게 됩니다. 10.0.1.7/32와 같이 명시하십시오.
  • 터널 자체의 주소와 겹치는 범위입니다. gluetun 문서에서는 이 경우 VPN 트래픽이 브리지를 통해 외부로 나가게 되어 포트 포워딩이 중단된다고 경고합니다. 사설 대역을 열기 전에 WIREGUARD_ADDRESSES 값을 먼저 확인하십시오.
  • Tailscale을 위한 100.64.0.0/10 설정입니다. 이는 단 하나의 피어에 접근하기 위해 약 400만 개의 주소를 여는 것과 같습니다. 필요한 피어만 /32 항목으로 나열하십시오.

이 설정은 전체 네임스페이스에 적용된다는 점을 기억하십시오. 인덱서가 호스트 서비스에 접근할 수 있도록 서브넷을 열면, 동일한 네임스페이스를 공유하는 토렌트 클라이언트에게도 같은 서브넷이 열리게 됩니다. 이 변수를 변경할 때마다 공인 IP 확인 절차를 다시 수행하십시오. 변경 사항이 의도한 대로 적용되었는지 확인할 수 있는 유일한 방법이기 때문입니다.

gluetun을 재시작할 때 발생하는 문제

gluetun은 네트워크 네임스페이스를 소유하므로, gluetun의 생명 주기가 곧 네임스페이스의 생명 주기가 됩니다. gluetun이 중단된 상태에서 의존 컨테이너를 시작하면 즉시 실패합니다.

Error response from daemon: cannot join network of a non running container

gluetun을 그 자리에서 재시작하는 것은 더 조용한 실패를 유발합니다. 의존 컨테이너들은 계속 실행 중이지만, 이들이 연결된 네임스페이스는 그 아래에서 재구축되기 때문에 docker ps는 모든 것이 정상이라고 보고하지만 실제로는 아무런 응답도 하지 않게 됩니다. gluetun 서비스에 변경 사항이 발생하면, 일부분만 재시작하지 말고 전체 그룹을 다시 생성하십시오.

docker compose up -d --force-recreate

이미지 업데이트 시에도 동일하게 적용됩니다. 새로운 gluetun 이미지를 내려받고 해당 서비스만 다시 생성하면, 다른 서비스들은 더 이상 존재하지 않는 네임스페이스를 가리키게 됩니다.

FAQ

Docker에서 "port publishing and the container type network mode"라는 오류가 발생하는 이유는 무엇입니까?

ports: 블록이 network_mode: service:gluetun을 설정한 서비스에 남아 있기 때문입니다. 포트를 게시하면 호스트 포트를 컨테이너의 네트워크 네임스페이스로 전달하는 NAT 규칙이 추가되는데, 해당 모드의 컨테이너는 별도의 네임스페이스를 갖지 않습니다. 해당 서비스에서 ports: 블록을 삭제하고 동일한 매핑을 gluetun 서비스에 추가하십시오. 애플리케이션은 여전히 공유 네임스페이스 내부에서 해당 포트를 수신 대기하므로 포트 번호는 동일하게 유지됩니다.

gluetun 뒤에 있는 서비스에 다른 컨테이너가 어떻게 접근합니까?

동일한 네임스페이스 내의 컨테이너들은 127.0.0.1을 통해 서로 통신합니다. 외부 컨테이너는 gluetun 서비스 이름을 사용하므로 http://qbittorrent:8080은 작동하지 않지만 http://gluetun:8080는 작동합니다. 애플리케이션 컨테이너는 Docker 네트워크상에 주소를 가지지 않으므로, 내장 DNS 서버가 해당 컨테이너 이름에 대해 해석할 주소가 없습니다. 두 컨테이너가 동일한 Compose 네트워크를 공유한다면 이를 위해 포트를 게시할 필요는 없습니다.

FIREWALL_OUTBOUND_SUBNETS에는 무엇을 입력해야 합니까?

gluetun 뒤의 컨테이너가 연결을 시작해야 하는 주소만 가능한 한 좁은 범위로 작성하십시오. 단일 머신은 /32로 표기합니다. 흔히 사용되는 두 가지 항목은 172.17.0.1/32의 Docker 호스트와 연결하려는 각 Tailscale 피어에 대한 /32입니다. 0.0.0.0/0를 추가하지 마십시오. 또한 VPN 자체 터널 주소와 겹치는 범위를 추가해서도 안 됩니다. 게시된 포트로 들어오는 인바운드 연결은 여기에 항목을 추가할 필요가 없습니다.

컨테이너가 Tailscale MagicDNS 이름을 해석하지 못하는 이유는 무엇입니까?

MagicDNS는 호스트의 리졸버를 Tailscale의 DNS 서버로 지정하여 작동하는데, 컨테이너는 호스트의 리졸버를 사용하지 않습니다. 컨테이너는 자체 /etc/resolv.conf 설정에 명시된 값을 사용하며, gluetun 뒤에서는 gluetun의 DNS 설정이 적용됩니다. docker exec <container> cat /etc/resolv.conf으로 이를 확인하십시오. 피어의 숫자형 100.x 주소를 사용하거나, 해당 컨테이너에 extra_hosts 항목을 추가하여 이름을 고정하십시오.

트래픽이 여전히 VPN을 통과하고 있는지 어떻게 확인합니까?

네임스페이스 내부에서 요청을 하나 실행하고 호스트에서 동일한 요청을 실행한 뒤 결과를 비교하십시오. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org는 VPN 제공업체의 출구 주소를 반환해야 하며, VPS에서 curl -s https://api.ipify.org을 실행하면 VPS의 주소가 반환되어야 합니다. 두 결과가 일치한다면 터널이 컨테이너의 트래픽을 전달하지 않는 것입니다. FIREWALL_OUTBOUND_SUBNETS을 변경할 때마다 이 확인 절차를 다시 수행하십시오.