Dev 컨테이너가 VPN 전용 호스트에 닿지 않을 때: 어디를 볼까
브라우저와 컨테이너는 리졸버, 프록시, 경로 컨텍스트가 서로 다릅니다. VPN 설정을 바꾸기 전에 실패하는 프로세스가 어느 쪽을 쓰는지 먼저 확인하세요.
사설 API가 데스크톱 브라우저에서는 열리지만 개발 컨테이너에서는 실패합니다. 이는 모순이 아니며, VPN 자체가 원인인 경우도 드뭅니다. 브라우저 요청과 실패한 요청은 서로 다른 프로세스가 만들었고, 리졸버와 프록시, 경로가 다르며, 흔히 완전히 다른 네트워크 네임스페이스에서 나옵니다. “내 노트북에서는”이라는 말은 Windows 호스트, WSL 배포판, Docker Desktop 가상 머신, 그리고 그 VM 안의 컨테이너 중 하나를 뜻할 수 있습니다. 같은 호스트 이름에 네 가지 컨텍스트가 각각 다르게 답할 수 있는 것입니다. 설정을 하나도 건드리기 전에 실패하는 프로세스가 어느 쪽을 쓰는지 먼저 밝혀내세요.
설정을 바꾸기 전에 프로세스를 찾으세요
VPN 클라이언트가 아니라 컨테이너 엔진부터 시작하세요. 명령줄이 어느 컨텍스트와 대화하는지, 그 엔진이 어떤 운영체제에서 도는지 물어보세요. 데스크톱 VM이 관리하는 Linux 컨테이너는 로컬 데몬이 관리하는 컨테이너와 다른 경로를 지나기 때문입니다:
docker context ls
docker info --format '{{.OperatingSystem}} / {{.Name}}'
docker run --rm alpine ip route
Windows에서는 wsl -l -v이 배포판이 WSL 2 아래에서 도는지 알려 줍니다. 이는 배포판을 자체 가상 스위치 뒤에 두는 구성입니다. 이 세부 사항이 컨테이너가 호스트의 라우팅 테이블을 공유하는지, 아니면 호스트에 닿기 위해 포워딩된 포트나 호스트 이름이 필요한 NAT 뒤에 있는지를 결정합니다.
요청 하나를 고정하고 비교하세요
사설 호스트의 무해한 엔드포인트 하나를 골라 모든 비교에 쓰세요. 브라우저에서, 호스트 터미널에서, WSL 배포판에서, 그리고 컨테이너 안에서입니다. 시도마다 네 가지를 기록하세요. 해석된 주소, 프록시 사용 여부, 실패 유형, 경과 시간입니다. 이름 조회, 연결 수립, TLS 검증, HTTP 상태는 서로 다른 네 가지 문제이며, 이것을 “안 된다”로 뭉뚱그리면 오전 시간이 통째로 사라집니다.
비교는 실패하는 컨테이너 안에서, 타임아웃을 걸어 실행하세요. 조용한 방화벽이 터미널을 몇 분씩 붙잡지 않도록 하기 위해서입니다. 새 컨테이너는 엔진의 네트워크 컨텍스트를 재현하지만 애플리케이션 컨테이너의 네임스페이스, 프록시 변수, 리졸버까지 재현하지는 않습니다. 따라서 실제 컨테이너에 docker exec하는 것이 권위 있는 검증이고, 사이드카는 차이가 엔진 쪽인지 컨테이너 쪽인지 분리하는 수단일 뿐입니다:
docker exec -it app /bin/sh -c "getent hosts api.internal; curl -v --max-time 8 https://api.internal/ 2>&1 | tail -20"
docker run --rm --network container:app curlimages/curl:latest -sv --max-time 8 https://api.internal/ 2>&1 | tail -20
alpine 기본 이미지에는 busybox가 있고 curl은 없습니다. 복사한 진단 블록이 실패하는 것은 바로 이런 세부 사항 때문입니다. 클라이언트가 들어 있는 이미지를 고르거나 같은 실행 안에서 추가하세요. 무관한 공개 사이트와의 비교는 아무것도 분리하지 못합니다. 컨테이너에서 https://example.com가 통한다면, 컨테이너에 인터넷 접근이 있다는 것만 알 뿐 터널을 통해 사설 주소에 닿는지는 알 수 없습니다. 마찬가지로 새 터미널을 연다는 것은 그 터미널이 동작한다는 증거일 뿐, 실패한 애플리케이션과 같은 런타임을 공유한다는 증거가 아닙니다.
Docker Desktop은 VPN과 함께 동작할 수 있습니다
Docker 자체의 VPN과 호스트 연결에 관한 문서도 이를 지원되는 구성으로 설명합니다. 솔직히 요약하면 자동으로 깨지는 것이 아니라 동작한다는 쪽입니다. 데스크톱 VM이 설계상 호스트 터널을 우회한다고 가정하지 마세요. 많은 구성에서 트래픽은 호스트 터널을 올바르게 따릅니다. 그래서 “Docker는 VPN을 무시한다”는 속설이 자신감 넘치는 잘못된 조언을 낳습니다.
여전히 중요한 것은 사용 중인 백엔드, 클라이언트의 스플릿 터널 정책, 그리고 VM이 받은 리졸버 구성입니다. 경로는 설치하지만 DNS는 설치하지 않는 VPN 클라이언트, 또는 VM이 닿을 수 없는 DNS 서버를 밀어 넣는 클라이언트는 컨테이너 안에서 이름 조회 실패를 만듭니다. 반면 호스트는 추가 리졸버나 캐시된 응답이 있어서 동작합니다. 이것은 컨테이너 옷을 입은 리졸버 문제입니다. VPN을 다시 연결해도 해결되지 않습니다. 터널은 이미 요청한 트래픽을 나르고 있기 때문입니다.
주소 중복과 MTU는 다른 장애입니다
두 장애는 흔히 접근 정책으로 오해되므로 따로 진단할 가치가 있습니다. 첫째는 주소 중복입니다. VPN이 컨테이너 네트워크의 기본 브리지 서브넷과 충돌하는 대역을 밀어 넣으면, 그 대역에 대한 컨테이너 경로는 로컬 브리지를 가리키고 터널에는 결코 닿지 않습니다. 추측하지 말고 실제 서브넷을 확인하세요:
docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
ip route get 10.20.30.40
둘째는 MTU 문제입니다. 연결이 맺어지고 TLS가 끝나는데 더 큰 응답만 멈춘다면 그것은 패킷 크기 문제이지 권한 문제가 아니며, DNS 레코드를 바꿔도 절대 고쳐지지 않습니다. 무엇이든 바꾸기 전에 작은 요청과 큰 요청으로 시험하고 두 결과를 모두 기록하세요.
중복의 해결책은 주소 계획의 소유자가 고르는 다른 컨테이너 서브넷이나 범위가 제한된 경로입니다. MTU 멈춤의 해결책은 경로를 견디는 값까지 터널이나 인터페이스의 MTU를 조이거나 낮추는 것이며, 자신이 통제하는 계층에서 적용합니다.
프록시 설정은 머신 단위가 아니라 프로세스 단위입니다
컨테이너 안에서 localhost은 그 컨테이너 자체의 네트워크 컨텍스트를 가리키므로, 호스트에서 도는 프록시는 그 이름으로는 닿을 수 없습니다. Docker Desktop은 호스트 접근용으로 host.docker.internal을 제공하고, Linux에서는 --add-host=host.docker.internal:host-gateway으로 명시적으로 추가합니다. 어느 쪽이든 그 이름은 호스트로 가는 경로를 줄 뿐, 프록시 서비스를 만들지도, 그것을 쓸 권한을 주지도 않습니다.
실제 환경에 맞게 제공된 프록시 주소와 포트를 쓰고, 누가 그것을 읽는지 확인하세요. 대화형 셸에서 export한 변수는 모든 에디터 확장, 언어 서버, 빌드 도구, 백그라운드 데몬을 자동으로 설정하지 않습니다. 중요한 것은 실패한 요청을 만든 프로세스이므로, 자기 터미널이 아니라 그 프로세스의 환경을 살펴보세요.
마지막으로, 수정 자체가 설명이 되게 하세요. 쓸모 있는 해결책은 무엇이 잘못된 경계였는지, 어떤 설정이 그것을 바꿨는지, 이제 어떤 프로세스 집합에 영향을 주는지를 이름으로 짚습니다. VPN과 에디터, 엔진을 차례로 재시작하면 증상은 달라질 수 있지만, 다음 사람이 쓸 수 있는 것은 아무것도 남지 않습니다.