Clash Docker透明代理进阶配置:解决镜像拉取超时
面向开发者与运维工程师的 Clash Docker 代理实践,解析容器流量如何经过宿主机代理,并给出 Docker Engine、Linux 与 CI/CD 环境中的可用配置和常见故障排查方法。
先分清:容器代理与透明代理不是一回事
Docker 中出现镜像拉取超时,第一步不是马上修改 Clash 的 YAML,而是先确认哪一段流量没有经过代理。宿主机上的 Clash 能正常访问网页,并不代表 Docker 容器、Docker Engine 或 CI 任务也会自动使用这条代理链路。浏览器通常会读取系统代理设置,Docker 容器却只认识自己的网络命名空间;Docker Engine 拉取镜像时,发起请求的进程甚至不在普通业务容器里。
实际链路可以拆成三种情况。第一种是客户端代理:只给某个命令或应用设置 HTTP_PROXY、HTTPS_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_PROXY 和 HTTPS_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-get、npm install、pip 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 正常、认证正常”的顺序逐层验证。每一步都先确认结果,再进入下一步,避免同时修改多个变量后无法判断真正原因。
- 在宿主机执行
curl -I --proxy http://127.0.0.1:7890 https://registry-1.docker.io/v2/,确认 Clash 入站端口和当前节点能访问目标仓库。 - 在临时容器内使用
host.docker.internal:7890重复请求,判断 Docker 网桥是否能到达宿主机代理。 - 执行
docker info和systemctl show --property=Environment docker,确认 dockerd 已加载代理,而不是只有当前 Shell 设置了变量。 - 查看 Clash 日志。容器或 dockerd 发起请求时,日志中应出现对应目标域名或连接记录;完全没有记录,说明流量尚未进入 Clash。
- 使用
docker run --rm alpine:3.20 nslookup registry-1.docker.io检查 DNS。解析超时、返回错误地址或被 fake-ip 误处理,都可能表现为后续 TLS 超时。 - 检查宿主机时间、证书链和 MTU。时间偏差会导致 TLS 握手失败,过小或过大的 MTU 则可能让 HTTPS 在握手后卡住。
- 最后再切换节点或代理模式做对照。若 HTTP 代理成功、TUN 失败,问题集中在透明路由;若宿主机成功、容器失败,问题集中在监听地址、防火墙或网桥。
| 现象 | 优先检查 | 常见修复 |
|---|---|---|
| 宿主机成功,容器 Connection refused | Clash 监听地址、容器到宿主机地址 | 开启允许局域网访问,使用 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 劫持和入站端口设置逐项验证。