Clash Docker透明代理进阶配置:解决镜像拉取超时

面向开发者与运维工程师的 Clash Docker 代理实践,解析容器流量如何经过宿主机代理,并给出 Docker Engine、Linux 与 CI/CD 环境中的可用配置和常见故障排查方法。

先分清:容器代理与透明代理不是一回事

Docker 中出现镜像拉取超时,第一步不是马上修改 Clash 的 YAML,而是先确认哪一段流量没有经过代理。宿主机上的 Clash 能正常访问网页,并不代表 Docker 容器、Docker Engine 或 CI 任务也会自动使用这条代理链路。浏览器通常会读取系统代理设置,Docker 容器却只认识自己的网络命名空间;Docker Engine 拉取镜像时,发起请求的进程甚至不在普通业务容器里。

实际链路可以拆成三种情况。第一种是客户端代理:只给某个命令或应用设置 HTTP_PROXYHTTPS_PROXY,流量通过 Clash 的 HTTP 或 mixed 入站端口转发。第二种是Docker Engine 代理:由 dockerd 进程使用代理,因此影响 docker pull、镜像认证和部分构建过程。第三种是透明代理:Clash mihomo 通过 TUN、iptables 或 nftables 接管指定网卡上的连接,应用不需要设置代理变量。

这三种方式不能混为一谈。给容器设置 HTTP_PROXY,并不会自动代理 Docker Engine;开启宿主机 TUN,也不一定能接管 Docker bridge 网桥转发的流量;在容器里写 127.0.0.1:7890,指向的还是容器自身,而不是宿主机。镜像拉取超时通常就是因为代理地址写错、代理只监听回环地址,或者 Docker 流量没有进入 TUN 的路由范围。

先确认请求是谁发起的

执行 docker pull 时,主要请求由宿主机上的 Docker Engine 发出;执行 RUN curl ... 时,请求由构建容器内的进程发出;应用容器运行后的请求,则由该容器自己的网络命名空间发出。三者需要分别配置,不能只改一个位置。

宿主机与 Clash 入站端口的基础配置

要让 Docker 访问宿主机上的 Clash,Clash 必须监听一个 Docker 可以到达的地址。许多客户端默认只监听 127.0.0.1,这对宿主机浏览器没有问题,却无法被 bridge 网络中的容器访问。mihomo 配置中常见的入站写法如下:

mixed-port: 7890
allow-lan: true
bind-address: "*"
mode: rule
log-level: info

mixed-port 同时提供 HTTP 与 SOCKS5 代理,适合 Docker 和命令行工具;allow-lan: true 允许来自非本机接口的连接;bind-address: "*" 让入站监听在可用网卡上。不同客户端的字段名称和安全默认值可能略有差异,修改后应在客户端的配置检查页面确认 YAML 能正常加载。

开放局域网监听意味着同一网络中的其他设备可能尝试连接该端口,因此不要把端口直接暴露到公网。Linux 宿主机可以先查看监听状态:

ss -lntp | grep 7890

如果结果只有 127.0.0.1:7890,容器通常无法通过宿主机网关访问它;如果显示 0.0.0.0:7890 或宿主机局域网地址,才具备被 Docker 网络访问的前提。Windows 和 macOS 使用 Docker Desktop 时,容器访问宿主机通常可以使用特殊主机名 host.docker.internal,Linux 原生 Docker 则建议显式添加宿主机映射。

为 Linux 容器声明宿主机网关

Docker Compose 中推荐使用 host-gateway,不要把某个当前网卡地址硬编码进配置。下面的例子让容器内的 host.docker.internal 解析到 Docker 宿主机,再通过宿主机的 Clash 7890 端口转发:

services:
  api:
    image: alpine:3.20
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      HTTP_PROXY: http://host.docker.internal:7890
      HTTPS_PROXY: http://host.docker.internal:7890
      ALL_PROXY: socks5://host.docker.internal:7890
      NO_PROXY: localhost,127.0.0.1,::1,.local,host.docker.internal

普通 HTTP 请求可以使用 HTTP_PROXYHTTPS_PROXY;部分支持 SOCKS5 的程序读取 ALL_PROXY。环境变量名称虽然通常不区分大小写,但不同程序的实现并不完全一致,生产环境可以同时设置大写和小写版本。NO_PROXY 要排除本地回环、服务发现域名、内部网段和数据库地址,避免内部请求绕一圈代理后失败。

先用临时容器验证网络,不要直接在复杂业务镜像中排查:

docker run --rm \
  --add-host host.docker.internal:host-gateway \
  -e HTTPS_PROXY=http://host.docker.internal:7890 \
  -e HTTP_PROXY=http://host.docker.internal:7890 \
  curlimages/curl:8.10.1 \
  -I https://registry-1.docker.io/v2/

如果这里返回 401 Unauthorized,反而通常说明已经成功到达镜像仓库,只是仓库要求认证;如果是 Connection refused,优先检查 Clash 是否监听外部地址和宿主机防火墙;如果是超时,再检查代理节点、DNS 解析和 Docker 网桥到宿主机的路由。

配置 Docker Engine:解决 docker pull 超时

docker pull 的代理配置必须交给 dockerd。即使当前终端已经执行了 export HTTPS_PROXY=...,也不代表后台 Docker Engine 会继承这些变量。Linux 使用 systemd 管理 Docker 时,可以创建 drop-in 配置目录和服务文件:

sudo mkdir -p /etc/systemd/system/docker.service.d

sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf >/dev/null <<'EOF'
[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,registry.local,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

这里的 127.0.0.1 指的是 Docker Engine 所在的宿主机,因此只有在 Clash 也运行于同一台宿主机上时才成立。如果 Clash 跑在另一台设备,应改为该设备的局域网地址,并确保 Clash 的 allow-lan 和防火墙允许访问。

重启后先检查 systemd 是否真的加载了变量:

systemctl show --property=Environment docker
docker info | sed -n '/HTTP Proxy/,+4p'

部分 Docker 版本会在 docker info 中显示代理字段,systemd 输出则更接近服务实际环境。修改后不要只重启终端,必须重新加载 systemd 并重启 Docker 服务。若使用 rootless Docker、Docker Desktop 或其他发行版打包方式,服务文件位置可能不同,应以 systemctl status docker 或对应桌面客户端的设置为准。

镜像仓库与构建请求要分开看

Docker Engine 的代理主要覆盖镜像仓库访问、认证和镜像元数据请求。Dockerfile 中 RUN apt-getnpm installpip install 等构建命令仍可能需要通过 build 参数或 BuildKit 配置传入代理,不能假设 dockerd 的代理会自动传进每一层构建容器。

Dockerfile、BuildKit 与 Compose 的代理传递

镜像拉取成功而构建阶段卡住,是另一类常见问题。构建过程至少有三条请求链:拉取基础镜像由 Engine 负责,执行 RUN 命令由临时构建容器负责,构建缓存或远程构建节点则可能由 BuildKit 守护进程负责。排查时应先看卡住的具体步骤,而不是笼统地认为“Docker 没走代理”。

临时验证可以在构建命令中传入代理参数:

docker build \
  --build-arg HTTP_PROXY=http://host.docker.internal:7890 \
  --build-arg HTTPS_PROXY=http://host.docker.internal:7890 \
  --build-arg NO_PROXY=localhost,127.0.0.1,.internal \
  -t demo-app:local .

Dockerfile 需要声明这些参数,构建阶段才能读取:

ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY

RUN apk add --no-cache curl

Docker 对代理构建参数有专门处理机制,通常不会像普通业务变量那样直接写进最终镜像环境;但仍不建议把带账号密码的代理地址硬编码进 Dockerfile、提交记录或公开 CI 日志。若代理需要认证,应优先使用 CI 的受保护变量,并注意构建输出可能打印完整命令行。

Compose 构建配置可以这样写:

services:
  worker:
    build:
      context: .
      args:
        HTTP_PROXY: http://host.docker.internal:7890
        HTTPS_PROXY: http://host.docker.internal:7890
        NO_PROXY: localhost,127.0.0.1,.internal
    environment:
      HTTP_PROXY: http://host.docker.internal:7890
      HTTPS_PROXY: http://host.docker.internal:7890
      NO_PROXY: localhost,127.0.0.1,.internal

build.args 只作用于构建阶段,environment 作用于容器运行阶段,两者缺一不可。对于多阶段构建,参数需要在实际执行网络请求的阶段重新声明;对于远程 BuildKit,代理地址必须是构建节点能够访问的地址,不能填开发者电脑上的 127.0.0.1

透明代理方案:TUN、网桥与 nftables 的边界

如果容器内程序不支持 HTTP 代理变量,或者希望统一接管多个服务,可以考虑透明代理。但透明代理比设置环境变量复杂得多,至少涉及路由、DNS、转发和回环流量四个部分。mihomo 的 TUN 适合接管宿主机流量,但 Docker bridge 发出的数据包是否进入 TUN,取决于内核路由、网卡绑定和客户端实现,不能只勾选 TUN 开关就认为容器已经代理。

Linux 宿主机可以先确认 Docker 网桥和转发状态:

ip addr show docker0
ip route
sysctl net.ipv4.ip_forward
docker network inspect bridge

容器默认通过 docker0 出站,源地址通常是 172.17.0.0/16 或自定义网段。若 mihomo 只监听回环接口,网桥流量无法连接到代理入站;若防火墙默认丢弃 FORWARD,容器即使能解析域名也无法建立外连;若 DNS 没有被接管,域名解析可能在进入代理前就失败。

启用 TUN 时,mihomo 配置通常需要明确自动路由和 DNS 劫持:

tun:
  enable: true
  stack: mixed
  auto-route: true
  auto-detect-interface: true
  dns-hijack:
    - any:53

dns:
  enable: true
  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16
  nameserver:
    - https://192.0.2.53/dns-query

这段配置的重点是让需要透明接管的 DNS 请求先进入 Clash,再由规则决定解析路径。192.0.2.53 仅作为文档中的保留地址示例,实际使用时必须替换为当前环境可用的 DNS 上游。Docker 内部的服务发现域名、*.local、数据库主机名等,不应盲目返回 fake-ip;可以通过 fake-ip-filter 排除,否则容器可能无法访问同一 Compose 网络中的服务。

不建议直接复制网上的整套 iptables 规则。Docker、firewalld、ufw 和 nftables 可能同时管理转发链,手工插入规则容易造成重复 NAT、回环或容器间通信中断。更稳妥的顺序是:先用宿主机代理端口验证连通,再确认 Docker 网桥转发,最后只为确实不支持代理变量的流量增加透明接管。

CI/CD 环境中的可复用配置

CI 任务通常运行在临时 Runner、虚拟机或 Kubernetes Pod 中,不能假定它们能访问开发机上的 Clash。最可靠的方式是让 Runner 所在网络提供一个稳定的代理地址,或者在同一执行节点上运行 mihomo,再把代理地址通过受保护变量注入任务。

以通用 Shell 任务为例:

export HTTP_PROXY="http://proxy.internal:7890"
export HTTPS_PROXY="http://proxy.internal:7890"
export NO_PROXY="localhost,127.0.0.1,.internal,.svc,10.0.0.0/8"

docker info
docker pull alpine:3.20
docker build \
  --build-arg HTTP_PROXY="$HTTP_PROXY" \
  --build-arg HTTPS_PROXY="$HTTPS_PROXY" \
  --build-arg NO_PROXY="$NO_PROXY" \
  -t registry.internal/team/app:"$CI_COMMIT_SHA" .

CI 中的 NO_PROXY 尤其重要。内部镜像仓库、Runner 控制端、Kubernetes API、服务发现域名和缓存服务通常不应经过外部代理。不要只写一个短主机名;有些程序使用完整域名,有些程序直接连接 IP,需要同时覆盖域名后缀和必要网段。

如果 Runner 运行在容器里,代理地址不能使用宿主机的 127.0.0.1。Docker Compose 可通过 extra_hosts 映射宿主机;Kubernetes 则应使用集群内可达的代理 Service。若代理端口需要认证,凭据应放入 CI Secret,避免出现在 YAML、构建日志和生成的镜像层中。

从 DNS 到 TLS:一套可重复的排查顺序

镜像拉取超时不一定是 Clash 节点问题,建议按照“地址可达、代理可达、解析正常、TLS 正常、认证正常”的顺序逐层验证。每一步都先确认结果,再进入下一步,避免同时修改多个变量后无法判断真正原因。

  1. 在宿主机执行 curl -I --proxy http://127.0.0.1:7890 https://registry-1.docker.io/v2/,确认 Clash 入站端口和当前节点能访问目标仓库。
  2. 在临时容器内使用 host.docker.internal:7890 重复请求,判断 Docker 网桥是否能到达宿主机代理。
  3. 执行 docker infosystemctl show --property=Environment docker,确认 dockerd 已加载代理,而不是只有当前 Shell 设置了变量。
  4. 查看 Clash 日志。容器或 dockerd 发起请求时,日志中应出现对应目标域名或连接记录;完全没有记录,说明流量尚未进入 Clash。
  5. 使用 docker run --rm alpine:3.20 nslookup registry-1.docker.io 检查 DNS。解析超时、返回错误地址或被 fake-ip 误处理,都可能表现为后续 TLS 超时。
  6. 检查宿主机时间、证书链和 MTU。时间偏差会导致 TLS 握手失败,过小或过大的 MTU 则可能让 HTTPS 在握手后卡住。
  7. 最后再切换节点或代理模式做对照。若 HTTP 代理成功、TUN 失败,问题集中在透明路由;若宿主机成功、容器失败,问题集中在监听地址、防火墙或网桥。
现象优先检查常见修复
宿主机成功,容器 Connection refusedClash 监听地址、容器到宿主机地址开启允许局域网访问,使用 host-gateway
容器能访问代理,docker pull 仍超时dockerd 环境变量配置 systemd drop-in 后 reload 并重启 Docker
基础镜像能拉取,RUN 下载失败BuildKit 与构建参数同时传入 build args 和运行环境变量
域名解析失败,IP 连接正常DNS 监听、劫持和 fake-ip 过滤确认 DNS 进入 mihomo,排除内部域名
HTTPS 卡在握手阶段节点、证书、系统时间和 MTU先用 curl 输出详细日志,再逐项对照
内部服务也被送进代理NO_PROXY 与 Clash 规则补充内部域名、网段和 fake-ip-filter

完成配置后,建议把代理验证写进部署前检查:先测试代理端口,再测试仓库认证,最后进行一次最小镜像构建。不要用“网页能打开”作为 Docker 代理已经生效的判断标准;只有请求在正确的进程、正确的网络命名空间和正确的代理入站端口之间闭环,镜像拉取超时问题才算真正解决。

继续检查客户端与配置

如果宿主机代理、Docker Engine 和容器内请求仍然表现不一致,可以先确认当前客户端使用的是 mihomo 内核,再对照 TUN、DNS 劫持和入站端口设置逐项验证。

下载Clash