Docker 안에서 실행되는 개발 서버와 CI 작업은 호스트의 프록시 설정을 자동으로 상속하지 않습니다. 컨테이너 내부에서 HTTP_PROXY를 지정하는 방법도 있지만, 모든 프로그램이 환경 변수를 읽는 것은 아니며 패키지 관리자, 서브프로세스, DNS 요청이 서로 다른 경로를 사용할 수 있습니다. 이때 Clash 투명 프록시를 구성하면 애플리케이션을 수정하지 않고도 컨테이너의 TCP 트래픽을 가로채 프록시 그룹과 규칙에 따라 전달할 수 있습니다. 이 글에서는 Mihomo를 기준으로 Docker 네트워크 구조, REDIRECT와 TPROXY 방식, DNS 처리, CI에서의 운영 방법, 그리고 YAML·iptables·컨테이너 권한 문제를 단계별로 정리합니다.
Docker 투명 프록시의 기본 구조
일반적인 Docker 컨테이너는 별도의 네트워크 네임스페이스와 가상 이더넷 인터페이스를 사용합니다. 컨테이너에서 외부 주소로 연결하면 패킷은 컨테이너의 eth0를 거쳐 Docker 브리지인 docker0로 이동하고, 호스트의 NAT 규칙을 통과한 뒤 외부 네트워크로 나갑니다. 따라서 Clash를 호스트에서 실행하더라도 컨테이너 트래픽이 자동으로 Clash 포트에 도착하지는 않습니다.
투명 프록시는 이 경로에 개입해 특정 목적지 포트의 연결을 Clash의 투명 프록시 포트로 리디렉션합니다. Clash는 원래 목적지 정보를 확인한 다음 도메인, IP, 규칙 제공자, 프록시 그룹을 기준으로 최종 경로를 선택합니다. 중요한 점은 애플리케이션이 프록시를 인식하지 못해도 된다는 것입니다. curl, Git, npm, pip, Go 모듈 도구처럼 프록시 환경 변수를 읽지 않는 프로그램도 같은 규칙을 적용받습니다.
- 호스트 실행 방식: Clash가 Docker 브리지와 같은 호스트 네트워크에서 패킷을 처리합니다. iptables 규칙을 직접 관리해야 하지만 여러 컨테이너에 적용하기 쉽습니다.
- 전용 게이트웨이 컨테이너: Clash 컨테이너를 네트워크 게이트웨이처럼 사용합니다. Compose 구조가 명확하지만 다른 컨테이너의 네트워크 모드를 조정해야 합니다.
- host 네트워크 방식: Linux에서 가장 단순한 방식입니다. Clash가 호스트 인터페이스를 직접 보지만, 포트 충돌과 격리 수준을 반드시 확인해야 합니다.
Mihomo YAML 핵심 설정
Docker 환경에서는 먼저 Clash의 일반 프록시 그룹과 DNS를 안정적으로 구성한 뒤, 투명 프록시용 리스너를 추가하는 순서가 좋습니다. 최신 Mihomo에서는 redir-port, tproxy-port, mixed-port를 목적에 맞게 나눌 수 있습니다. TCP만 처리하고 설정을 단순하게 유지하려면 REDIRECT 포트가 적합합니다. TCP와 UDP를 함께 가로채거나 원래 목적지 정보를 최대한 보존해야 한다면 TPROXY를 고려하세요.
mixed-port: 7890 allow-lan: true bind-address: 0.0.0.0 # TCP REDIRECT용 포트 redir-port: 7892 # TCP/UDP TPROXY용 포트 (Linux 커널과 iptables 설정 필요) tproxy-port: 7893 mode: rule log-level: info ipv6: false dns: enable: true listen: 0.0.0.0:1053 enhanced-mode: fake-ip fake-ip-range: 198.18.0.1/16 nameserver: - 1.1.1.1 - 8.8.8.8 fake-ip-filter: - '*.lan' - '*.local' - 'localhost.ptlogin2.qq.com' proxies: - name: proxy-main type: socks5 server: proxy.example.com port: 443 username: user password: password proxy-groups: - name: PROXY type: select proxies: - proxy-main - DIRECT rules: - DOMAIN-SUFFIX,github.com,PROXY - DOMAIN-SUFFIX,docker.io,PROXY - DOMAIN-SUFFIX,npmjs.org,PROXY - DOMAIN-SUFFIX,pypi.org,PROXY - GEOIP,LAN,DIRECT - MATCH,PROXY
allow-lan: true와 bind-address: 0.0.0.0은 다른 네트워크 네임스페이스에서 Clash 포트에 접근할 때 필요합니다. 그러나 관리 API인 external-controller까지 모든 인터페이스에 공개할 필요는 없습니다. API는 가능하면 127.0.0.1:9090에 바인딩하고, 원격 관리가 필요할 때만 인증 시크릿과 방화벽 규칙을 추가하세요.
REDIRECT와 TPROXY 선택 기준
REDIRECT는 iptables의 nat 테이블에서 TCP 목적지 포트를 로컬 포트로 변경합니다. 설정이 비교적 쉽고 HTTP, HTTPS, Git, 패키지 다운로드처럼 TCP 중심의 개발 트래픽에 충분합니다. 반면 UDP를 처리하지 못하고, 원래 목적지 주소를 얻기 위해 추가적인 소켓 옵션이나 복원 로직이 필요할 수 있습니다.
TPROXY는 패킷의 목적지를 유지한 채 로컬 소켓으로 전달하는 방식입니다. UDP와 TCP를 함께 다루고 DNS, QUIC, 게임 서버처럼 주소 보존이 중요한 환경에서 더 적합합니다. 대신 Linux 커널의 투명 소켓 지원, mangle 테이블, 정책 라우팅, 컨테이너 권한이 모두 맞아야 합니다. 처음 구성한다면 REDIRECT로 TCP 흐름을 검증한 다음 TPROXY로 확장하는 것이 안전합니다.
Docker Compose와 iptables 연결
호스트에서 Mihomo 컨테이너를 실행하고 Docker 브리지의 TCP 연결을 REDIRECT하는 예시는 다음과 같습니다. 실제 인터페이스 이름과 브리지 대역은 환경마다 다르므로 docker network inspect로 확인해야 합니다. 아래 예시에서 172.17.0.0/16은 기본 Docker 브리지 대역이며, Compose가 별도의 네트워크를 만들면 해당 CIDR로 바꿔야 합니다.
services: mihomo: image: metacubex/mihomo:latest container_name: mihomo restart: unless-stopped network_mode: host cap_add: - NET_ADMIN - NET_RAW volumes: - ./config:/root/.config/mihomo environment: - TZ=Asia/Seoul
network_mode: host을 사용하면 Clash가 호스트의 7892와 7893 포트를 직접 열 수 있습니다. NET_ADMIN은 컨테이너 내부에서 네트워크 규칙을 조작할 때 필요하며, 호스트의 iptables를 직접 변경하는 방식에서는 실행 위치에 따라 추가 권한이 필요합니다. 보안상 전체 privileged: true를 무조건 부여하기보다는 필요한 capability만 추가하는 편이 좋습니다.
# 컨테이너에서 발생한 웹 트래픽을 Clash REDIRECT 포트로 전달 iptables -t nat -N CLASH_DOCKER iptables -t nat -A CLASH_DOCKER -d 127.0.0.0/8 -j RETURN iptables -t nat -A CLASH_DOCKER -d 10.0.0.0/8 -j RETURN iptables -t nat -A CLASH_DOCKER -d 172.16.0.0/12 -j RETURN iptables -t nat -A CLASH_DOCKER -d 192.168.0.0/16 -j RETURN iptables -t nat -A CLASH_DOCKER -p tcp -j REDIRECT --to-ports 7892 # Docker 브리지에서 호스트로 나가는 TCP만 대상 iptables -t nat -A DOCKER-USER -s 172.17.0.0/16 -p tcp -j CLASH_DOCKER
사설 네트워크를 먼저 제외하는 이유는 컨테이너가 Docker DNS, 데이터베이스, 사내 서비스에 접근할 때 프록시를 거치지 않도록 하기 위해서입니다. 이 예외가 없으면 172.17.0.1의 호스트 서비스나 내부 레지스트리가 프록시로 전달되어 연결이 꼬일 수 있습니다. 또한 Docker가 재시작되면 사용자 규칙이 사라질 수 있으므로 운영 환경에서는 iptables-persistent, systemd 서비스, 또는 Docker 이벤트 기반 초기화 스크립트로 규칙을 복원해야 합니다.
DNS와 컨테이너 네트워크 점검
투명 프록시 장애의 상당수는 실제 프록시 연결이 아니라 DNS에서 발생합니다. 컨테이너의 /etc/resolv.conf에는 보통 Docker 내장 DNS인 127.0.0.11이 기록됩니다. 이 DNS는 외부 질의를 호스트 설정으로 전달하므로, Clash의 fake-ip 규칙을 적용하지 못할 수 있습니다. 특히 도메인 기반 규칙을 사용하면서 컨테이너가 먼저 로컬 DNS에서 실제 IP를 받은 경우, 이후 연결은 IP 기반으로 처리되어 예상과 다른 규칙이 선택될 수 있습니다.
간단한 개발 환경에서는 컨테이너의 DNS를 Clash DNS 포트로 지정할 수 있습니다. 단, host 네트워크에서 DNS 포트가 충돌하지 않는지 확인하고, DNS 요청 자체를 다시 투명 프록시로 리디렉션하는 순환 구조는 피해야 합니다.
services: build: build: . dns: - 172.17.0.1 environment: - HTTP_PROXY=http://172.17.0.1:7890 - HTTPS_PROXY=http://172.17.0.1:7890 - NO_PROXY=localhost,127.0.0.1,.local,172.17.0.0/16
위 설정에서 172.17.0.1은 호스트의 Docker 브리지 주소라는 전제입니다. 사용자 환경에서 다를 수 있으므로 다음 명령으로 확인하세요.
docker network inspect bridge docker exec -it build-container cat /etc/resolv.conf docker exec -it build-container getent hosts github.com docker exec -it build-container curl -Iv https://github.com docker logs --tail=100 mihomo
getent hosts로 이름 해석 여부를 확인하고, 그다음 curl -Iv로 TCP 연결을 확인하세요. 이름 해석은 성공하지만 연결이 실패하면 라우팅이나 방화벽 문제일 가능성이 높고, 이름 해석부터 실패하면 DNS 리스너 주소와 컨테이너의 resolv.conf를 먼저 확인해야 합니다.CI 환경에서의 운영과 장애 진단
CI에서는 작업마다 임시 컨테이너가 생성되고 삭제되므로 호스트에 영구적인 iptables 규칙을 두는 방식이 특히 중요합니다. 가장 안정적인 패턴은 러너 호스트에서 Mihomo를 항상 실행하고, Docker 브리지 대역 전체를 대상으로 규칙을 적용하는 것입니다. 빌드 컨테이너마다 별도의 프록시 컨테이너를 띄우면 구성은 격리되지만, 이미지 다운로드 전에 프록시 이미지 자체를 받아야 하는 순서 문제가 생길 수 있습니다.
패키지 다운로드 실패를 확인할 때는 다음 순서를 따르세요.
- 컨테이너 내부에서
ip route를 실행해 기본 경로와 게이트웨이가 존재하는지 확인합니다. getent hosts registry.npmjs.org또는 해당 패키지 저장소 도메인으로 DNS 응답을 확인합니다.curl -Iv로 TLS 연결을 시도하고, 인증서 오류인지 연결 거부인지 구분합니다.- Mihomo 로그에서 연결이 기록되는지 확인합니다. 로그에 전혀 나타나지 않으면 iptables 또는 네트워크 모드가 문제일 수 있습니다.
- 대시보드의 연결 목록에서 매칭 규칙과 실제 프록시 그룹을 확인합니다. 규칙은 위에서부터 평가되므로 마지막
MATCH전에 의도한 도메인 규칙을 배치해야 합니다.
CI에서 자주 발생하는 문제는 프록시 환경 변수와 투명 프록시가 동시에 적용되는 것입니다. 예를 들어 HTTPS_PROXY가 127.0.0.1:7890을 가리키는데, 컨테이너 내부의 localhost는 호스트가 아니라 컨테이너 자신을 의미합니다. 이 경우 환경 변수 방식은 실패하고, 투명 프록시만 정상 작동할 수 있습니다. 반대로 HTTP 프록시 연결도 iptables로 다시 REDIRECT하면 프록시 서버로 가는 연결이 재가로채기되어 무한 루프가 발생할 수 있습니다. Clash가 실행 중인 호스트 주소, 프록시 포트, NO_PROXY 범위를 명확히 분리하세요.
| 증상 | 가능한 원인 | 확인 방법 |
|---|---|---|
| 모든 외부 연결이 타임아웃 | REDIRECT 포트 미수신, iptables 체인 미연결 | iptables -t nat -L -n -v와 Mihomo 로그 확인 |
| 도메인 해석만 실패 | 컨테이너 DNS가 Clash에 도달하지 않음 | cat /etc/resolv.conf, DNS 리스너 확인 |
| 내부 DB까지 프록시됨 | 사설 CIDR 예외 누락 | Docker 브리지와 사내망 대역을 RETURN 처리 |
| 규칙이 항상 DIRECT로 매칭 | 도메인이 이미 IP로 해석되거나 규칙 순서 오류 | fake-ip와 규칙 순서를 함께 점검 |
| UDP 서비스만 작동하지 않음 | REDIRECT는 TCP만 처리 | TPROXY와 정책 라우팅 구성 검토 |
운영 환경에서는 설정 파일에 프록시 비밀번호를 평문으로 커밋하지 말고 Docker secret, CI secret 변수, 또는 런타임에 생성되는 별도 파일을 사용하세요. 이미지를 latest로 고정하면 예기치 않은 Mihomo 동작 변경이 발생할 수 있으므로, 검증한 버전을 명시하고 업데이트 전에 YAML 구문 검사와 다운로드 테스트를 실행하는 것도 좋습니다. 마지막으로 iptables 규칙에는 로그를 과도하게 추가하지 마세요. CI에서 대량의 패킷 로그가 쌓이면 디스크와 빌드 성능에 영향을 줄 수 있습니다.
Docker 투명 프록시는 단순히 포트 하나를 여는 작업이 아니라, 컨테이너 네트워크 경로와 DNS, 방화벽, Clash 규칙을 하나의 흐름으로 맞추는 작업입니다. 처음에는 TCP REDIRECT와 명시적인 도메인 규칙으로 작은 테스트 컨테이너를 검증하고, 이후 UDP가 필요한 서비스에만 TPROXY를 추가하면 문제 범위를 크게 줄일 수 있습니다. 구성이 안정화되면 개발 서버와 CI의 프록시 동작을 일관되게 유지하면서도 애플리케이션 코드에는 별도의 프록시 로직을 넣지 않아도 됩니다.
Clash로 트래픽을 완전히 제어하세요
Windows, macOS, Linux, Android, iOS 지원. 유연한 규칙, 간단한 설정.