典型现象:Clash 正常,Docker 却像「没代理」

很多开发者会遇到一种反差:Clash 在 macOS 或 Windows 上工作正常,浏览器访问海外站点、甚至终端里已正确设置 http_proxy 的工具都能连通,唯独 docker pull nginx、私有 registry 登录或 CI 风格的 docker buildx build 频繁失败。报错形态包括长时间卡在 Pulling fs layerTLS handshake timeouti/o timeout,以及偶发的证书或 SNI 相关错误。根因往往不在于镜像站「彻底不可用」,而是Docker 的请求路径没有走你期望的 Clash 出口,或被错误的 NO_PROXY、内网解析与网络模式放大成「间歇性失败」。

与 WSL2 里 apt/git 类似,Docker 场景还要额外区分三层:dockerd 守护进程发起的 registry 拉取、docker CLIconfig.json 中的客户端代理、以及 构建容器或 Compose 服务容器内部进程看到的网络环境。只改其中一层,都会在另一层表现为「仍然直连」或「仍然找不到代理」。下文按可复现顺序把三层串起来。

先建立心智模型:谁发起连接,就读谁的代理配置

docker pull 与向仓库推送镜像时,主要由 Docker Engine(dockerd)代表本机与远端 registry 建立 HTTPS 连接。若你只在自己的 zsh/bash 里 export HTTP_PROXY=...shell 里的变量不会自动注入 dockerd,除非通过 systemd 环境、Docker Desktop 图形界面或守护进程配置文件显式传递。反过来,某些发行版里为 dockerd 配好的代理,也不会自动进入 docker buildRUN 步骤启动的临时容器——那一层需要 Dockerfile 的 ARG/ENV 或 Compose 的 build.args

docker build 使用 BuildKit 时,还会牵涉构建器进程前端 Dockerfile 指令两套上下文:前者负责拉基础镜像、解析缓存,后者决定 RUN apt-get update 时是否看得到 http_proxy。因此排查时要先问「失败发生在 pull 基础镜像阶段,还是 RUN 某条命令阶段」,再决定改守护进程代理还是构建参数。对规则分流与策略组的整体设计,可继续参考《Clash YAML 规则分流完全指南》,把 registry 与常用 CDN 域名稳定送进合适策略组。

宿主机 Clash:混合端口、允许局域网与可达地址

在让 Docker 指向 Clash 之前,先确认从 Docker 视角能 TCP 连上你的代理端口。图形客户端通常提供 mixed-port(同时承载 HTTP CONNECT 与 SOCKS),适合作为 HTTP_PROXYALL_PROXY 的统一入口。若 Clash 仅监听 127.0.0.1,而 Docker 使用独立的 Linux VM 或网络命名空间(Docker Desktop 常见),容器或守护进程内的 127.0.0.1 并不等同于宿主机环回,需要改用 host.docker.internal(macOS/Windows 桌面版常用)或宿主机在桥接网段上的 网关 IP,并配合 Clash 开启允许局域网(Allow LAN),使代理监听在 0.0.0.0 或等价范围。

防火墙与「仅专用网络」规则也可能挡住来自 docker0 网桥的入站连接。建议先用一条简单命令在会跑 dockerd 的那套系统里探测:例如在宿主机上对 http://<HOST>:<PORT> 做 curl,再在进入容器的 shell 里重复(若已安装 curl)。若宿主成功而容器失败,优先回头检查监听地址、Allow LAN 与路由,而不是先改订阅节点。若你同时使用 TUN 接管系统流量,仍建议为 Docker 保留显式 HTTP 代理端口方案;TUN 与系统代理差异见《Clash TUN 与系统代理》

Docker Desktop:Engine 代理与 host.docker.internal

在 macOS 与 Windows 的 Docker Desktop 设置中,通常提供针对 Docker Engine 的 HTTP/HTTPS 代理配置入口,相当于把 HTTP_PROXY / HTTPS_PROXY / NO_PROXY 写给守护进程。代理地址可写为 http://host.docker.internal:7890(端口替换为你的 mixed 端口)。host.docker.internal 是 Docker Desktop 注入的特殊主机名,用于从容器或 Linux VM 侧解析到宿主机;这比手工写死局域网 IP 更耐 DHCP 变化,但仍需 Clash 对来自桥接网段的连接放行。

NO_PROXY 建议显式包含 localhost127.0.0.1::1、私有网段以及公司内部 registry 域名,避免访问本地或内网仓库时流量被误送到 Clash。若你使用私有 CA 或内网 TLS 拦截,错误地把这类主机名放进全局代理,可能表现为证书校验失败而非单纯的超时。

与 WSL2 场景对照 本站WSL2 代理文解决的是 Linux 子系统与 Windows 宿主之间的 127.0.0.1 语义;本文则面向 Docker 引擎与容器网桥。二者常在同一台机器上叠加出现,请分别核对端口与防火墙,不要混用一套「以为能通」的假设。

Linux 原生安装:为 dockerd 配置 systemd 环境

在纯 Linux 桌面或服务器上使用官方包安装的 Docker,常见做法是为 docker.service 增加 Environment="HTTP_PROXY=..." 的 drop-in 文件,或把变量写入 /etc/systemd/system/docker.service.d/http-proxy.confsystemctl daemon-reload && systemctl restart docker。此时代理地址通常是本机环回上的 Clash 端口http://127.0.0.1:7890),前提是 dockerd 与 Clash 在同一网络命名空间;若 Clash 跑在用户会话而 dockerd 跑在 system 作用域,仍需确认监听范围与权限是否允许本机其他进程连入。

在远程 Linux 上仅通过 SSH 使用 Docker、而 Clash 跑在本地笔记本的场景,则不应把笔记本文的局域网地址硬编码进服务器上的 dockerd——那是拓扑错误。此类情况更适合在出网已畅通的服务器上使用镜像加速或私有 registry 中继,或在 SSH 隧道层面解决,而不是照搬桌面 Clash 教程。

# Example: /etc/systemd/system/docker.service.d/http-proxy.conf
[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,172.16.0.0/12,192.168.0.0/16"

客户端 ~/.docker/config.json 与「仅 CLI」场景

部分团队更习惯在用户目录下维护 ~/.docker/config.jsonproxies 字段,为 default 或特定 Docker 上下文指定 httpProxy / httpsProxy / noProxy。这对客户端发起的部分操作有帮助,但与守护进程级代理的覆盖范围未必完全一致,具体行为随 Docker 版本略有差异。把它当作与 systemd/Desktop 并行、可审计的补充,而不是唯一真相来源,排错时更稳。

若你使用多上下文或多主机(远程 Docker),请在对应上下文中分别验证代理是否生效,避免「本机修好、CI 仍炸」的错位。与 npm/pnpm 等开发者工具链协同时的 NO_PROXY 习惯,可参考《Cursor 与 npm 开发者代理》,在容器构建场景同样值得抄作业。

# Snippet: ~/.docker/config.json (proxies block, adapt to your port)
{
  "proxies": {
    "default": {
      "httpProxy": "http://host.docker.internal:7890",
      "httpsProxy": "http://host.docker.internal:7890",
      "noProxy": "localhost,127.0.0.1,::1,.internal,.local"
    }
  }
}

docker build 与 buildx:构建阶段如何带上 HTTP_PROXY

多阶段构建里,RUN apt-getRUN curlRUN npm install 都在临时中间容器执行。要让这些命令走代理,常见做法包括:在 Dockerfile 顶部声明 ARG HTTP_PROXY / ARG HTTPS_PROXY,再 ENV HTTP_PROXY=$HTTP_PROXY;构建命令行使用 docker build --build-arg HTTP_PROXY=http://host.docker.internal:7890 ...。否则构建器会按「无代理直连」去访问 Debian、npm 或 PyPI 上游,表现即为偶发超时或与宿主机不一致的 TLS 错误。

buildx 与 BuildKit 启用时,注意代理变量需传递到前端后端两侧:有时还要为 builder 实例本身配置环境。若使用远程 builder 或 Kubernetes 上的构建驱动,宿主机 Clash 的地址不再适用,需要改为那套环境中的可达代理或内网出网策略。对仅在本机构建的用户,优先在文档中固定「--build-arg + Dockerfile ARG/ENV」的可复制模板,减少口口相传的遗漏。

# Dockerfile excerpt
ARG HTTP_PROXY
ARG HTTPS_PROXY
ENV HTTP_PROXY=$HTTP_PROXY HTTPS_PROXY=$HTTPS_PROXY

RUN apt-get update
# Build example (host shell)
docker build --build-arg HTTP_PROXY=http://host.docker.internal:7890 \
  --build-arg HTTPS_PROXY=http://host.docker.internal:7890 -t myimg .

Docker Compose:pull、build 与 services.environment 的分工

docker compose up 会依次触发拉取镜像构建镜像启动容器。若失败发生在第一步,应优先检查 Engine 级代理与 registry 认证;若失败发生在构建阶段,关注 build.args 与 Dockerfile;若容器已启动但内部应用访问外网失败,再改 services.<name>.environment 中的 HTTP_PROXY。把这三段混为一谈,容易出现在 compose 里写满环境变量、却仍然 pull timeout 的困惑。

对于需要在团队内共享的配置,可使用 .env 文件配合 compose 的变量替换,但切忌把包含凭证的代理账号提交到公开仓库。若 registry 在公司内网,确保 NO_PROXY 覆盖相应域名后缀,以免流量被送到境外节点再绕回,造成「极慢」而非「硬超时」。

# compose excerpt: build-time args vs runtime env
services:
  app:
    build:
      context: .
      args:
        HTTP_PROXY: http://host.docker.internal:7890
        HTTPS_PROXY: http://host.docker.internal:7890
    environment:
      HTTP_PROXY: http://host.docker.internal:7890
      HTTPS_PROXY: http://host.docker.internal:7890
      NO_PROXY: localhost,127.0.0.1,::1

bridge 与 host 网络:127.0.0.1 与 DNS 路径差异

默认 bridge 模式下,容器拥有独立网络命名空间,127.0.0.1 指向容器自身。要从容器访问宿主机上的 Clash,应使用 host.docker.internal(若可用)或宿主机在 docker0 网桥上的网关地址(常见为类似 172.17.0.1,以实际 ip addr 为准)。这与 WSL2、VPN、TUN 同时启用时的路由表相互影响,排错时应「先 ping/curl 代理端口,再谈规则分流」。

host 网络模式下,容器与宿主机共享同一网络栈,服务监听的端口直接暴露在宿主;此时应用若访问 127.0.0.1:7890,确实可能指向宿主机上的 Clash,但与 bridge 下 extra_hosts 的行为不同,Compose 文件迁移时要重新验证。host 模式还会改变 DNS 解析与防火墙策略,不适合作为「为了偷懒少配代理」的默认选项,除非你的 workload 明确要求。

NO_PROXY、内网 registry 与 DNS 的耦合

错误配置 NO_PROXY 的典型后果是:访问 harbor.company.local 仍被送进 Clash,结果因策略或证书问题失败;或反过来,把本该走代理的公网 registry 写进 NO_PROXY,导致仍然直连超时。建议用后缀形式(如 .company.local)覆盖内网域,并保留对 Clash DNS 与 fake-ip 模式的关注——若 registry 域名在 fake-ip 下解析异常,会先表现为「解析错 IP」而非代理超时,可对照《Clash DNS fake-ip 与 redir-host》中的本地解析章节。

建议的自检顺序(可打印)

  1. 宿主机上确认 Clash mixed 端口对局域网开放,curl 探活通过。
  2. 在同一台机器上对 host.docker.internal:端口 或网关 IP 再测一次。
  3. 不构建、先 docker pull 最小镜像,隔离 registry 与 Engine 代理问题。
  4. 再跑带 RUN wget/curl 的最小 Dockerfile,验证构建期代理。
  5. 最后启动 Compose 服务,在容器内检查 env | grep -i proxy 是否符合预期。

若只有第 1 步失败,修 Clash 监听与防火墙;若 1 成功而 2 失败,修 Docker 到宿主的连通;若 2 成功而 3 失败,重点查 dockerd 代理与 config.json;若 3 成功而 4 失败,回到 Dockerfile ARG/ENV 与 --build-arg;若前四步都成功而业务仍失败,才深入应用层协议与策略组日志。

常见问题速查

现象 更可能的原因 建议
docker pull 超时,浏览器正常 dockerd 未走代理或 NO_PROXY 不当 配置 Engine/Desktop 代理;收紧/放宽 NO_PROXY
构建阶段 RUN apt 失败 构建容器未继承 HTTP_PROXY Dockerfile ARG/ENV + --build-arg
容器内 curl 不通代理地址 误用 127.0.0.1 指宿主 改用 host.docker.internal 或网关 IP
TLS 握手失败、证书异常 企业 MITM 与代理链冲突 内网域名进 NO_PROXY;校验公司根证书

写在最后:让 Docker 与 Clash 各司其职

把问题拆成「守护进程拉镜像」「构建容器跑指令」「运行中服务出网」三层后,Docker 与宿主机 Clash 的配合会变得可预测:Clash 负责按规则与 DNS 策略把 TCP 连接送到合适节点,Docker 负责在正确的命名空间里把 HTTP_PROXY 指到真实可达的宿主端口。相比把代理再封装进镜像或 Sidecar,多数桌面开发者在单机上用「宿主 Clash + Engine 代理 + 明确的 build-arg」即可长期维护,排错路径也更短。相比其他同类工具,Clash 在规则可读性、日志与多协议上的组合,对同时要照顾浏览器、终端与容器工作负载的用户往往更顺手。

若你希望少在端口与防火墙细节上反复试错,可以优先选择带图形界面、能一键开关 Allow LAN 与查看 mixed 端口的客户端,把精力放在节点质量与规则命中验证上;全平台安装包可从本站下载中心获取,免费订阅链接也在下载页集中维护,导入后即可用于本文涉及的出网场景。

立即免费下载 Clash,开启流畅上网新体验;在宿主机配好混合端口与局域网访问后,按本文清单为 Docker Engine、buildx 与 Compose 对齐 HTTP_PROXYNO_PROXY,镜像拉取与构建也能稳定走同一套规则。

更多文章见技术专栏