증상: «Clash는 켜 두었는데 Docker만 레지스트리에 못 붙는다»
증상은 대개 세 가지로 묶입니다. 첫째, docker pull nginx 같은 기본 명령이 길게 멈췄다가 타임아웃한다. 둘째, docker build 중 RUN apt-get이나 베이스 이미지를 받는 단계에서 TLS 경고나 인증서 오류가 난다. 셋째, Docker Compose로 띄운 서비스만 외부 API에 닿지 못하고, 같은 맥락에서 호스트에서 직접 친 curl은 정상이다. 이 패턴은 «노드 품질이 나빠서»라기보다 트래픽이 Clash 쪽 규칙·출구에 들어가기 전에 이미 직접 회선으로 나가거나, 컨테이너 네임스페이스 안의 localhost가 호스트의 Clash 리슨 주소와 일치하지 않아 생깁니다.
Clash 쪽에서 할 일은 (1) mixed-port 또는 HTTP 포트가 실제로 열려 있는지, (2) Allow LAN이 꺼져 있어 가상 브리지에서 들어오는 연결이 거절되지는 않는지, (3) 방화벽이 해당 포트 인바운드를 막지 않는지입니다. 이 세 가지는 LAN 공유·방화벽 가이드와 맥락이 같습니다. 다만 Docker는 그 위에서 데몬 단위·빌드 단위·컨테이너 단위로 프록시 설정을 또 나눠야 하므로, 아래 순서대로 «어느 프로세스가 아웃바운드이냐」를 먼저 고정하는 것이 빠릅니다.
7890은 예시로만 바꿔 쓰세요.
왜 pull·build·런타임 설정을 나눠야 하나
docker pull은 사용자 셸의 HTTP_PROXY만으로는 안 될 때가 많습니다. 이유는 클라이언트가 dockerd에 API를 보내고, 실제 레지스트리와의 HTTPS 세션은 데몬 쪽에서(또는 구성에 따라 별도 프로세스에서) 열리기 때문입니다. 반면 docker build는 Classic 빌더와 BuildKit 활성 여부에 따라 외부 네트워크를 쓰는 주체가 달라지고, 멀티 스테이지·RUN --mount 같은 문법까지 섞이면 «어느 단계의 DNS·TLS가 실패했는지»를 로그에서 먼저 읽어야 합니다.
Docker Compose는 선언된 각 services가 서로 다른 네트워크·환경 변수를 가질 수 있습니다. 따라서 «Compose 파일 전체에 프록시를 한 번만 쓴다»는 발상은 빠르게 깨집니다. 한 서비스는 사내 레지스트리에만 가야 하고, 다른 서비스는 퍼블릭 허브에서 이미지를 받아야 할 수 있습니다. 이때는 environment를 서비스별로 두거나, no_proxy에 내부 호스트명을 넣어 불필요한 홉을 줄이는 편이 안전합니다. 개발자 워크스테이션에서 npm·git까지 한꺼번에 묶는 패턴은 Cursor·npm 개발자 분기 글과 겹치는 부분이 있으나, Docker는 커널 네트워크 스택과 네임스페이스가 달라 본문만으로도 충분히 독립된 주제입니다.
bridge와 host, 그리고 127.0.0.1의 함정
기본 bridge 네트워크에 올라간 컨테이너에서 http://127.0.0.1:7890은 «그 컨테이너 자신의 루프백»입니다. 호스트에서 돌아가는 Clash에 연결하려면 컨테이너가 도달 가능한 호스트 측 주소를 써야 합니다. Docker Desktop(Windows·macOS)은 종종 host.docker.internal 이름을 제공하고, Linux 네이티브 환경에서는 --add-host=host.docker.internal:host-gateway 또는 실제 이더넷·와이파이 인터페이스의 IPv4를 씁니다.
host 네트워크 모드(network_mode: host)를 쓰면 컨테이너가 호스트의 네트워크 스택을 공유하므로, 이 경우에 한해 127.0.0.1:포트가 호스트 Clash와 같은 의미를 가질 수 있습니다. 다만 포트 충돌·보안 경계가 달라지므로 운영 규칙과 상충하지 않는지 먼저 확인해야 합니다. «브라우저는 되는데 컨테이너만 안 된다»는 말의 절반은 이 주소 불일치에서 옵니다.
# Example: reach host Clash from a one-off container (replace port) docker run --rm --add-host=host.docker.internal:host-gateway \ -e HTTPS_PROXY=http://host.docker.internal:7890 \ alpine sh -c "wget -qO- --timeout=5 https://example.com | head"
위 한 줄이 성공하면 «컨테이너에서 호스트 프록시까지의 TCP 경로」는 열렸다고 볼 수 있습니다. 실패하면 Allow LAN·방화벽·호스트 IP를 다시 잡습니다.
1단계: Docker 데몬이 레지스트리에 나갈 때 — systemd(Linux)
Linux에서 패키지로 설치한 Docker는 dockerd에 환경 변수를 주입하는 방식이 일반적입니다. /etc/systemd/system/docker.service.d/http-proxy.conf 같은 drop-in에 [Service] 블록으로 Environment="HTTP_PROXY=..." 등을 넣고 systemctl daemon-reload 후 docker 유닛을 재시작합니다. 이렇게 하면 docker pull·이미지 레이어 다운로드가 데몬 설정을 따르기 쉬워집니다.
# /etc/systemd/system/docker.service.d/http-proxy.conf (example) [Service] Environment="HTTP_PROXY=http://127.0.0.1:7890" Environment="HTTPS_PROXY=http://127.0.0.1:7890" Environment="NO_PROXY=localhost,127.0.0.1,::1,10.0.0.0/8"
NO_PROXY에는 사내 레지스트리·로컬 레지스트리 미러 호스트를 넣어 두면 Clash를 거치지 않아야 할 트래픽을 줄일 수 있습니다. Linux 서버에 Clash를 직접 올리는 흐름은 Linux 헤드리스 mihomo·systemd와 연결해 읽을 수 있습니다.
Docker Desktop(Windows·macOS)에서의 데몬 프록시
Docker Desktop은 GUI의 Settings → Resources → Proxies(표기는 버전에 따라 다름)에서 시스템 프록시를 따르거나 수동 URL을 넣는 방식을 제공합니다. 여기서 호스트 Clash의 HTTP 포트를 넣으면 docker pull 경로가 안정되는 경우가 많습니다. Windows에서는 WSL 백엔드와 VM 사이에서 주소 해석이 달라질 수 있으므로, «데스크톱 앱에 표시된 포트」와 «WSL 내부에서 보이는 localhost»가 같은지 WSL2 가이드의 미러형 네트워킹 절을 함께 확인하는 것이 좋습니다.
2단계: ~/.docker/config.json — 클라이언트 측 프록시 힌트
데몬 설정과 별도로, Docker CLI는 ~/.docker/config.json의 proxies 블록을 참조할 수 있습니다. 팀에서 동일한 클라이언트 동작을 강제할 때 유용합니다. 다만 엔진 버전·클라이언트 버전에 따라 적용 범위가 조금씩 다르므로, 변경 후에는 반드시 docker pull 한 번으로 검증하세요.
{
"proxies": {
"default": {
"httpProxy": "http://127.0.0.1:7890",
"httpsProxy": "http://127.0.0.1:7890",
"noProxy": "localhost,127.0.0.1,registry.internal"
}
}
}
원격 SSH로 빌드하는 경우에는 해당 사용자 홈의 config.json이 적용됩니다. CI에서는 시크릿으로 덮어쓰는 패턴이 일반적입니다.
3단계: BuildKit·buildx — 빌드 단계용 HTTP_PROXY
DOCKER_BUILDKIT=1이 기본인 환경이 많습니다. BuildKit이 켜져 있으면 Dockerfile의 RUN이 외부로 나갈 때 빌드 시점의 프록시가 필요합니다. 이때는 docker buildx build에 --build-arg HTTP_PROXY=...를 넘기거나, Compose의 build.args에 같은 키를 둡니다. 베이스 이미지를 레지스트리에서 받는 단계와 RUN curl이 외부로 나가는 단계가 서로 다른 레이어에 있으므로, 로그상 어느 줄에서 끊기는지로 원인을 나눕니다.
export DOCKER_BUILDKIT=1 docker buildx build \ --build-arg HTTP_PROXY=http://127.0.0.1:7890 \ --build-arg HTTPS_PROXY=http://127.0.0.1:7890 \ -t myimg:latest .
호스트에서 빌드할 때는 127.0.0.1이 Clash를 가리키는 경우가 많지만, 원격 빌더나 CI에 보내면 그 환경의 localhost가 달라집니다. 그때는 빌더가 있는 머신의 프록시 주소로 바꿔야 합니다.
4단계: Docker Compose — 서비스별 environment와 build
Compose 파일에서 한 서비스에만 프록시를 주려면 해당 services.<name>.environment 아래에 HTTP_PROXY·HTTPS_PROXY·NO_PROXY를 명시합니다. 이미지를 미리 받는 단계는 호스트의 docker compose pull이 담당하므로, «런타임에만 프록시가 필요한 앱」과 «빌드 시에만 필요한 패키지 설치」를 혼동하지 않도록 주석을 달아 두면 팀 협업에 유리합니다.
services: app: build: context: . args: HTTP_PROXY: http://host.docker.internal:7890 HTTPS_PROXY: http://host.docker.internal:7890 environment: HTTPS_PROXY: http://host.docker.internal:7890 NO_PROXY: localhost,api.internal
Linux에서 host.docker.internal이 없다면 앞 절의 extra_hosts 또는 실제 호스트 IP로 바꿉니다. 정책상 프록시를 쓰면 안 되는 내부 HTTP 호출이 있다면 NO_PROXY에 도메인을 촘촘히 넣어야 합니다.
검증 순서와 흔한 실수
디버깅 순서를 한 줄로 압축하면 다음과 같습니다. (1) 호스트 셸에서 curl -x http://127.0.0.1:7890 -I https://registry-1.docker.io처럼 Clash 경유가 되는지 확인한다. (2) 한 번 띄운 컨테이너에서 host.docker.internal 또는 호스트 IP로 같은 테스트를 반복한다. (3) docker pull 로그가 데몬 쪽 오류인지 클라이언트 오류인지 구분한다. (4) 빌드만 실패하면 BuildKit 로그의 스테이지 번호를 본다.
흔한 실수로는 SOCKS 포트에 http:// 스킴을 붙이는 경우, Clash가 재시작되며 포트가 바뀌었는데 Compose 파일에 옛 숫자가 남은 경우, 그리고 회사 SSL 검사 장비와 프록시 체인이 겹쳐 MITM이 중복되는 경우가 있습니다. TLS 오류 메시지가 의심스러우면 일단 동일 노드로 브라우저 접속과 openssl s_client 출력을 비교해 보세요.
자주 묻는 질문
컨테이너 안에서 http_proxy=http://127.0.0.1:7890을 썼는데도 안 됩니다.
기본 브리지에서는 컨테이너의 127.0.0.1이 호스트와 다릅니다. 호스트 Clash에 붙으려면 host.docker.internal·호스트 LAN IP·host-gateway 매핑 등 실제로 라우팅되는 주소를 써야 합니다.
docker pull은 되는데 docker build만 레지스트리에서 끊깁니다.
pull과 build는 서로 다른 프로세스 경로를 탈 수 있습니다. BuildKit 사용 시 빌드 인자로 HTTP_PROXY를 넘기고, 데몬 프록시와 중복·충돌이 없는지 확인하세요.
Docker Compose에서 서비스 하나만 프록시를 타게 하려면?
해당 서비스 블록에만 environment를 두면 됩니다. 다른 서비스는 영향을 받지 않습니다.
Clash 구독 링크는 정상인데 Docker만 느립니다.
구독 URL과 컨테이너 아웃바운드는 별개입니다. 클라이언트에서 프로필·규칙을 최신으로 유지하되, Docker는 데몬·CLI·빌드·런타임 설정을 추가로 맞춰야 합니다. 구독 갱신 이슈만 따로 보려면 구독 자동 갱신 트러블슈팅을 참고하세요.
마무리
Docker와 Clash를 같이 쓸 때 핵심은 «브라우저가 따라오는 시스템 프록시»와 «엔진·빌드·컨테이너가 각각 쓰는 주소」를 동일하게 가정하지 않는 것입니다. HTTP_PROXY·HTTPS_PROXY·NO_PROXY를 어디에 두었는지만 명확히 해도 타임아웃과 TLS 오류의 절반은 줄어듭니다. 장기적으로는 팀 내에 예시 Compose 스니펫과 no_proxy 목록을 공유해 두면 온보딩 비용이 크게 줄어듭니다.
규칙 기반으로 트래픽을 나누는 Clash 쪽 YAML 전반은 Clash YAML 규칙 분기 가이드에서 다루고 있으며, 본 글의 프록시 주소 문제와 맞물리면 «엔진은 프록시를 타는데 규칙이 DIRECT로 떨어진다»는 다음 단계 디버깅으로 이어집니다.