Docker 컨테이너에서 이미지를 내려받거나 외부 API에 연결할 때 i/o timeout, context deadline exceeded, TLS 핸드셰이크 지연이 반복된다면 컨테이너마다 프록시 환경 변수를 추가하는 것보다 호스트 네트워크 경로를 정리하는 편이 효과적입니다. 이 글에서는 Clash 또는 Mihomo가 실행 중인 Linux Docker 호스트에서 컨테이너 트래픽을 투명 프록시로 전달하는 방법을 설명합니다. Docker 브리지 네트워크, Clash의 DNS, daemon.json, iptables 규칙을 함께 구성하여 Docker Hub 이미지 풀 실패와 컨테이너의 외부 연결 지연을 해결하고, 프록시를 우회하는 네트워크 누수까지 점검합니다.

Docker 투명 프록시의 작동 방식

일반적인 Docker 프록시는 두 가지 방식으로 나뉩니다. 첫 번째는 Docker 데몬에 HTTP 또는 HTTPS 프록시를 지정하는 방식입니다. 이 방법은 Docker Hub에서 이미지를 가져오는 dockerd 자체에는 효과적이지만, 실행 중인 컨테이너의 모든 연결을 자동으로 처리하지는 않습니다. 두 번째는 호스트의 라우팅 테이블과 방화벽 규칙을 이용해 컨테이너의 TCP 연결을 Clash로 리다이렉트하는 투명 프록시 방식입니다.

Docker의 기본 브리지 네트워크에서는 컨테이너가 별도의 사설 주소를 받습니다. 예를 들어 호스트의 물리 인터페이스가 192.168.1.20이고 Docker 브리지가 172.17.0.1/16이라면 컨테이너는 172.17.0.2 같은 주소를 사용합니다. 컨테이너가 인터넷에 연결할 때 패킷은 대체로 다음 경로를 따릅니다.

  1. 컨테이너가 도메인 이름을 DNS 서버에 질의합니다.
  2. 컨테이너가 얻은 IP 주소로 TCP 연결을 생성합니다.
  3. Docker의 NAT 규칙이 출발지 주소를 호스트 주소로 변환합니다.
  4. 호스트의 기본 라우팅 경로를 통해 패킷이 외부 네트워크로 전송됩니다.

투명 프록시를 적용하면 세 번째 단계 전후에서 패킷을 가로채 Clash의 redir-port 또는 Mihomo의 tproxy 포트로 보냅니다. Clash는 연결의 원래 목적지를 확인한 뒤 도메인 규칙과 IP 규칙을 적용합니다. 따라서 컨테이너 내부에 HTTP_PROXY, HTTPS_PROXY를 설정하지 않아도 대부분의 TCP 트래픽을 동일한 정책으로 처리할 수 있습니다.

먼저 범위를 구분하세요: docker pull이 실패하는 문제는 Docker 데몬 프록시 설정이 필요할 수 있고, 컨테이너 안의 curl, 패키지 관리자, 애플리케이션 연결이 실패하는 문제는 투명 프록시와 DNS 구성이 필요할 수 있습니다. 두 문제는 서로 다른 프로세스의 트래픽입니다.

Clash와 Mihomo에서 Docker용 포트 구성

Linux 호스트에서 가장 이해하기 쉬운 시작점은 TCP 리다이렉트입니다. Clash가 redir-port를 열면 방화벽의 REDIRECT 대상이 될 수 있습니다. 최신 Mihomo에서는 TPROXY를 사용해 TCP와 UDP를 함께 처리할 수도 있지만, TPROXY는 정책 라우팅과 방화벽 마크 설정이 추가로 필요합니다. Docker 이미지 다운로드와 일반적인 HTTP, HTTPS 요청부터 해결하려면 먼저 TCP 리다이렉트로 구성한 뒤 필요할 때 TPROXY로 확장하는 것이 안전합니다.

Clash 기본 포트 설정
mixed-port: 7890              # 애플리케이션용 HTTP/SOCKS 포트
redir-port: 7892               # TCP REDIRECT 대상
allow-lan: true
bind-address: '*'
mode: rule
log-level: info

rules:
  - DOMAIN-SUFFIX,docker.io,Proxy
  - DOMAIN-SUFFIX,docker.com,Proxy
  - DOMAIN-SUFFIX,ghcr.io,Proxy
  - DOMAIN-SUFFIX,googleapis.com,Proxy
  - GEOIP,LAN,DIRECT
  - MATCH,Proxy

여기서 Proxy는 실제로 존재하는 프록시 그룹 이름으로 바꿔야 합니다. Docker Hub는 여러 도메인과 CDN을 사용하므로 docker.io만 추가한다고 항상 충분하지 않습니다. 연결 화면에서 실제 목적지를 확인하면서 registry-1.docker.io, auth.docker.io, 이미지 저장소 CDN 도메인을 점검하세요. 사내 레지스트리나 LAN 서비스는 프록시로 보내지 않도록 GEOIP,LAN,DIRECT와 내부 도메인 규칙을 앞쪽에 배치하는 것이 좋습니다.

Docker DNS와 fake-ip 예외

컨테이너가 사용하는 DNS가 호스트의 로컬 스텁 리졸버를 잘못 가리키면 패킷 리다이렉트가 정상이어도 이름 해석 단계에서 멈출 수 있습니다. 특히 호스트의 /etc/resolv.conf127.0.0.53이 들어 있고 Docker 네임스페이스에서 해당 주소에 접근할 수 없는 경우가 흔합니다. Clash의 DNS 포트를 LAN 또는 Docker 브리지에서 접근 가능한 주소로 열고 Docker에 명시적으로 전달하는 편이 안정적입니다.

Docker에 Clash DNS 지정
dns:
  enable: true
  listen: 0.0.0.0:1053
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  fake-ip-filter:
    - '*.lan'
    - '*.local'
    - 'localhost.ptlogin2.qq.com'
    - 'time.*.com'
  nameserver:
    - https://1.1.1.1/dns-query
    - https://8.8.8.8/dns-query

위 예시의 DNS 포트는 기존 서비스와 충돌하지 않도록 1053을 사용했습니다. Docker 컨테이너에서 호스트의 브리지 주소가 172.17.0.1이라면 컨테이너의 DNS 서버를 172.17.0.1:1053으로 지정할 수 있습니다. 다만 Docker의 --dns 옵션은 포트 번호를 지정할 수 없으므로, 실제 운영 환경에서는 호스트의 53번 포트에 Clash DNS를 바인딩하거나 별도의 DNS 포워더를 두어 53번에서 1053번으로 전달해야 합니다.

fake-ip을 사용할 때는 내부 서비스, 서비스 검색, 일부 게임과 VoIP 도메인을 fake-ip-filter에 추가해야 합니다. 반대로 Docker Hub 같은 외부 도메인은 fake IP를 통해 도메인 기반 규칙을 적용하는 편이 유리합니다. 컨테이너가 숫자 IP를 직접 사용하면 도메인 정보가 사라져 도메인 규칙이 적용되지 않을 수 있으므로, 필요한 경우 해당 IP 대역에 별도 IP-CIDR 규칙을 추가하세요.

iptables로 Docker 트래픽 리다이렉트하기

이제 실제로 컨테이너의 TCP 연결을 Clash로 보냅니다. 핵심은 Docker 브리지에서 나오는 패킷을 사용자 정의 체인으로 보내고, Clash 프로세스의 UID가 만든 패킷과 로컬 네트워크 목적지를 제외하는 것입니다. Clash가 다시 외부 연결을 만들 때 그 패킷까지 재차 리다이렉트하면 무한 루프가 발생하므로 반드시 Clash 실행 사용자 또는 보호 대상 그룹을 제외해야 합니다.

Docker 브리지 TCP 리다이렉트
# Docker 브리지 인터페이스와 Clash 포트
BRIDGE:=docker0
CLASH_PORT:=7892

# 사용자 정의 체인 생성
sudo iptables -t nat -N CLASH_DOCKER 2>/dev/null || true
sudo iptables -t nat -F CLASH_DOCKER

# Clash 프로세스 사용자가 만든 패킷은 제외
sudo iptables -t nat -A CLASH_DOCKER -m owner --uid-owner clash -j RETURN

# 사설 네트워크와 멀티캐스트는 직접 연결
sudo iptables -t nat -A CLASH_DOCKER -d 127.0.0.0/8 -j RETURN
sudo iptables -t nat -A CLASH_DOCKER -d 10.0.0.0/8 -j RETURN
sudo iptables -t nat -A CLASH_DOCKER -d 172.16.0.0/12 -j RETURN
sudo iptables -t nat -A CLASH_DOCKER -d 192.168.0.0/16 -j RETURN
sudo iptables -t nat -A CLASH_DOCKER -d 224.0.0.0/4 -j RETURN

# 나머지 TCP 연결을 Clash redir-port로 전달
sudo iptables -t nat -A CLASH_DOCKER -p tcp -j REDIRECT --to-ports ${CLASH_PORT}

# Docker 브리지에서 출발한 트래픽에 체인 연결
sudo iptables -t nat -A PREROUTING -i ${BRIDGE} -p tcp -j CLASH_DOCKER

시스템에 따라 Clash가 clash가 아닌 다른 사용자로 실행될 수 있습니다. ps -eo user,comm | grep -E 'clash|mihomo'로 실제 사용자를 확인하고 --uid-owner 값을 수정하세요. Docker Compose가 사용자 정의 네트워크를 만들면 인터페이스 이름이 br-xxxxxxxx 형태로 생성될 수 있습니다. ip linkdocker network inspect 네트워크이름으로 올바른 브리지 인터페이스를 확인해야 합니다.

iptables 규칙은 재부팅 후 사라질 수 있습니다. Debian과 Ubuntu에서는 iptables-persistent 또는 배포판의 네트워크 초기화 서비스를 사용해 저장하고 복원하세요. Docker가 시작될 때 자체 NAT 규칙을 다시 구성하므로, 사용자 체인을 Docker 서비스 시작 이후에 적용하는 별도 systemd unit을 두는 것도 안전한 방법입니다.

주의: Docker의 DOCKER-USER 체인은 필터 테이블에 있으며 NAT 리다이렉트 자체를 대신할 수 없습니다. 또한 원격 SSH로 방화벽 명령을 실행할 때는 현재 SSH 세션을 차단하지 않도록 관리 포트와 LAN 대역을 먼저 허용하세요.

daemon.json으로 이미지 풀 프록시 설정

투명 프록시를 구성했더라도 docker pull이 계속 실패한다면 Docker 데몬에 명시적인 프록시를 추가할 수 있습니다. Docker 데몬은 컨테이너 프로세스와 다른 서비스이므로, 쉘에서 export HTTPS_PROXY를 실행하는 것만으로는 데몬에 전달되지 않습니다. 일반적으로 systemd drop-in 또는 Docker의 설정 파일을 사용합니다.

Docker daemon.json 예시
{
  "proxies": {
    "http-proxy": "http://127.0.0.1:7890",
    "https-proxy": "http://127.0.0.1:7890",
    "no-proxy": "localhost,127.0.0.1,::1,172.17.0.0/16,192.168.0.0/16,.local"
  }
}

파일 위치는 배포판과 Docker 설치 방식에 따라 다르지만 일반적인 경로는 /etc/docker/daemon.json입니다. JSON에는 주석을 넣을 수 없고, 이미 다른 설정이 있다면 기존 객체와 합쳐야 합니다. 수정 후 다음 순서로 데몬을 재시작합니다.

  1. sudo python3 -m json.tool /etc/docker/daemon.json으로 JSON 문법을 검사합니다.
  2. sudo systemctl daemon-reload를 실행합니다.
  3. sudo systemctl restart docker로 Docker 데몬을 재시작합니다.
  4. docker info에서 Proxy 항목을 확인합니다.
  5. docker pull alpine:latest로 이미지 다운로드를 테스트합니다.

Clash의 mixed-port는 HTTP 프록시와 SOCKS5를 동시에 지원하므로 데몬 프록시에는 보통 http://127.0.0.1:7890을 사용합니다. redir-port는 일반 HTTP 프록시 포트가 아니므로 daemon.json의 프록시 URL로 지정하면 안 됩니다. 또한 Docker 데몬이 별도의 네트워크 네임스페이스나 원격 호스트에서 실행되는 경우 127.0.0.1은 Clash가 실행 중인 호스트를 가리키지 않을 수 있습니다. 그때는 접근 가능한 호스트 주소와 방화벽 정책을 함께 확인해야 합니다.

연결 테스트와 네트워크 누수 점검

설정이 끝났다고 바로 성공으로 판단하지 말고 DNS, TCP, HTTPS, 이미지 레지스트리의 네 단계를 각각 확인하세요. 다음 명령은 일시적인 테스트 컨테이너에서 실행할 수 있습니다.

컨테이너 연결 테스트
# DNS 해석 확인
docker run --rm alpine:latest nslookup registry-1.docker.io

# HTTPS 연결과 응답 헤더 확인
docker run --rm curlimages/curl:latest \
  curl -I --max-time 15 https://registry-1.docker.io/v2/

# 컨테이너가 관찰하는 외부 주소 확인
docker run --rm curlimages/curl:latest \
  curl --max-time 15 https://ifconfig.me

# Docker 데몬의 레지스트리 인증 경로 확인
docker pull hello-world

Clash 대시보드의 Connections 화면에서는 컨테이너의 요청이 어떤 규칙과 프록시 그룹을 사용했는지 확인할 수 있습니다. 예상과 달리 DIRECT로 표시되면 DNS가 다른 주소를 반환했거나, Docker 브리지가 방화벽 체인에 연결되지 않았거나, 목적지가 제외 목록에 들어간 것입니다. 반대로 모든 LAN 접근까지 프록시로 보내면 내부 레지스트리, 데이터베이스, 서비스 디스커버리 연결이 느려질 수 있으므로 사설 대역은 의도적으로 직접 연결해야 합니다.

DNS 누수는 컨테이너 내부에서 /etc/resolv.conf를 확인하고 호스트에서 패킷을 관찰해 점검할 수 있습니다. 컨테이너가 공용 DNS의 53번 포트로 직접 UDP 요청을 보내고 있다면 Clash의 DNS 정책을 우회하고 있는 것입니다. 이 경우 Docker의 DNS 설정, 호스트의 포워딩 규칙, Clash DNS 리스너를 차례로 확인하세요. UDP 기반 QUIC 트래픽은 TCP REDIRECT 규칙으로 처리되지 않을 수 있으므로 브라우저나 패키지가 QUIC을 사용하는 환경에서는 UDP 차단, HTTP/3 비활성화, 또는 Mihomo TPROXY 구성을 별도로 검토해야 합니다.

문제 분리 방법: 먼저 curl -x http://127.0.0.1:7890로 명시적 프록시가 작동하는지 확인하고, 그 다음 일반 curl을 컨테이너에서 실행하세요. 명시적 프록시는 성공하지만 일반 연결만 실패한다면 노드 문제가 아니라 NAT, 브리지 인터페이스, DNS 또는 TPROXY 규칙 문제일 가능성이 높습니다.

자주 묻는 질문

daemon.json과 투명 프록시를 둘 다 설정해야 하나요?

항상 필요한 것은 아닙니다. 목표가 실행 중인 컨테이너의 모든 TCP 트래픽을 처리하는 것이라면 투명 프록시가 핵심입니다. 반대로 Docker Hub 이미지 다운로드만 실패한다면 daemon.json만으로 해결될 수 있습니다. 운영 환경에서는 데몬 프록시와 컨테이너 투명 프록시를 함께 사용하되, 같은 요청이 이중 프록시를 거치지 않는지 확인하세요.

컨테이너에서 DNS만 실패하고 호스트는 정상입니다. 이유가 무엇인가요?

가장 흔한 원인은 컨테이너가 호스트의 127.0.0.53 스텁 리졸버를 참조하는 경우입니다. Docker 네트워크에서 접근 가능한 Clash DNS 주소를 사용하거나 Docker 데몬의 DNS 설정을 별도로 지정하세요. 또한 Clash DNS 리스너가 Docker 브리지 주소에서 연결을 받고 있는지와 53번 포트 충돌 여부를 확인해야 합니다.

이 구성이 UDP와 QUIC도 처리하나요?

위의 REDIRECT 예시는 주로 TCP를 대상으로 합니다. UDP와 QUIC까지 투명하게 처리하려면 Mihomo의 TPROXY, 적절한 fwmark, 정책 라우팅 테이블, 커널 포워딩 설정이 필요합니다. 구성이 완전하지 않은 상태에서 UDP 규칙만 추가하면 연결이 불안정해질 수 있으므로 TCP 구성을 먼저 검증하는 것이 좋습니다.

내부 레지스트리와 데이터베이스는 어떻게 직접 연결하나요?

172.16.0.0/12, 사내 LAN 대역, 내부 도메인 suffix를 Clash의 DIRECT 규칙과 iptables의 RETURN 목록에 함께 추가하세요. 한쪽에만 예외를 넣으면 DNS는 직접 처리되지만 TCP는 프록시로 가거나, 반대로 TCP는 직접 연결되지만 DNS가 외부로 나가는 불일치가 발생할 수 있습니다.

Docker 투명 프록시는 단순히 포트 하나를 여는 작업이 아니라 Docker 데몬, 브리지 NAT, DNS, Clash 라우팅 정책을 하나의 경로로 맞추는 작업입니다. 먼저 데몬의 이미지 풀 경로를 확인하고, 다음으로 컨테이너 DNS와 TCP 리다이렉트를 검증한 뒤, 마지막으로 UDP와 내부 네트워크 예외를 확장하세요. 운영에 들어가기 전에는 규칙과 방화벽 설정을 저장하고 재부팅 후에도 동일하게 복원되는지 테스트하면 예기치 않은 직접 연결과 네트워크 누수를 크게 줄일 수 있습니다.

시작하기

Clash로 트래픽을 완전히 제어하세요

Windows, macOS, Linux, Android, iOS 지원. 유연한 규칙, 간단한 설정.

무료 다운로드 설정 가이드 보기 →