Advanced Clash Docker Proxy Setup For Faster Image Pulls

A practical guide for developers and DevOps engineers who need reliable Docker Hub access through Clash. Configure host and container traffic, automate CI usage, and troubleshoot failed image pulls.

The Traffic Model: Where Docker and Clash Meet

Docker image pulls are often blamed on Docker Hub, DNS, or a slow proxy node, but the first question is simpler: which process is actually opening the connection? The Docker CLI runs on the host, while image layers are downloaded by the Docker daemon. On a Linux server, that daemon usually runs as dockerd with root privileges and may not inherit the shell's HTTP_PROXY or HTTPS_PROXY variables. A browser can work through Clash while docker pull still attempts a direct connection and eventually times out.

There are three traffic paths to distinguish. The first is host-side traffic: commands and applications running directly on Windows, macOS, or Linux. The second is daemon traffic: requests made by Docker Engine to registry endpoints, token services, and storage hosts while pulling an image. The third is container traffic: connections made by processes inside a running container, such as package managers, build scripts, or application-side API clients. Configuring only one path does not automatically configure the other two.

For a normal image pull, the daemon commonly contacts the registry API, follows an authentication challenge, obtains a bearer token, and then downloads manifests and layers from one or more content hosts. Therefore, allowing only one hostname is not always enough. A proxy must be applied to the Docker daemon itself, and the selected Clash rules must cover the registry, authentication service, and layer-download destinations.

Traffic sourceTypical configuration pointWhat it affects
Docker CLI on the hostShell variables or Docker client environmentCLI requests and commands that contact a registry directly
Docker daemonsystemd drop-in, Docker Desktop settings, or daemon environmentImage pulls, pushes, registry authentication, and layer downloads
Build processBuild arguments and BuildKit settingsPackage downloads and remote requests made during docker build
Running containerContainer environment or Compose configurationOutbound traffic generated after the container starts
Transparent network pathClash TUN or redirection with correct routesTraffic that does not understand HTTP or SOCKS proxy variables

Start with the daemon, not the browser

Before changing DNS or downloading a different client, test the Docker daemon's path. If curl works with a proxy but docker pull fails, the problem is usually that dockerd has no proxy environment or cannot reach the Clash listener from its own network namespace.

Configure the Host and Docker Daemon Proxy

For a Linux host, the most predictable approach is to expose a Clash HTTP or mixed-port listener on an address reachable by Docker's daemon. A common local configuration uses mixed-port: 7890, which accepts both HTTP proxy and SOCKS5-style connections according to the client and protocol used. If Docker is running on the same host, 127.0.0.1:7890 can work for a system service. If Clash runs in another container, a virtual machine, or a separate host, use the reachable address instead.

Do not assume that localhost means the same thing everywhere. A Docker container's 127.0.0.1 refers to that container, not the host. A systemd-managed Docker daemon normally runs in the host network namespace, but Docker Desktop places the engine inside a managed virtual machine. This difference explains why one proxy address can work on Linux and fail on macOS or Windows.

On a Linux system using systemd, create a Docker service drop-in:

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

sudo tee /etc/systemd/system/docker.service.d/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,127.0.0.0/8,::1,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8,.local"
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker

HTTP_PROXY and HTTPS_PROXY are both commonly set to the Clash HTTP endpoint, even when the destination is HTTPS. The variable describes the proxy protocol used to reach the destination; it does not mean that HTTPS traffic becomes plain HTTP. NO_PROXY should contain local addresses, internal registry names, and private service domains that must not leave the local network. Avoid adding a broad public suffix unless there is a clear operational reason, because an overly large bypass list can silently send registry traffic direct.

If Clash listens only on 127.0.0.1, a local Linux Docker daemon can usually reach it. If the daemon is inside a virtual machine or Clash is bound to the host's LAN address, configure a listener address and firewall rule that match that topology. Binding a proxy to all interfaces can make it reachable by other machines, so access control matters. Prefer a protected LAN interface, firewall restrictions, and authentication where supported instead of exposing an unauthenticated proxy to the public internet.

Verify the Effective Docker Environment

Restarting the service is not enough; verify the environment that systemd actually attached to the daemon. The following commands reveal the service state and show whether Docker can reach the Clash listener:

systemctl status docker --no-pager
systemctl show docker --property=Environment
curl -x http://127.0.0.1:7890 -I https://registry-1.docker.io/v2/
docker pull hello-world

A registry response such as 401 Unauthorized from the curl command can be a healthy sign: it means the request reached the registry and the registry is asking for authentication. A connection timeout, connection refused error, or inability to resolve the proxy host indicates a path or listener problem instead. Run journalctl -u docker -f in another terminal while retrying the pull so that connection failures can be correlated with the daemon log.

Build Clash Rules for Registry and Layer Traffic

Once the Docker daemon can reach Clash, routing rules decide whether registry requests use a proxy group. A minimal rule set can route known registry domains to a group while keeping private networks direct. The exact set of layer hosts varies by registry, account, region, and content delivery arrangement, so avoid treating one hostname as a permanent complete list.

proxy-groups:
  - name: DOCKER
    type: select
    proxies:
      - "Preferred-Node"
      - "Fallback-Node"
      - DIRECT

rules:
  - DOMAIN,registry-1.docker.io,DOCKER
  - DOMAIN,auth.docker.io,DOCKER
  - DOMAIN,hub.docker.com,DOCKER
  - DOMAIN-SUFFIX,docker.io,DOCKER
  - DOMAIN-SUFFIX,docker.com,DOCKER
  - IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
  - IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
  - IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
  - MATCH,DIRECT

This example deliberately keeps DIRECT as an option in the Docker group. It is useful for comparing a node with a direct route during diagnosis, but it should not be selected when the local network cannot reliably reach the registry. If the configuration already has a global proxy group, point the Docker rules at that group instead of creating a duplicate group with a different naming convention.

Rule order is important. Clash evaluates rules from top to bottom and uses the first match. Place explicit Docker domains before a broad MATCH rule. A DOMAIN-SUFFIX rule covers the root domain and its subdomains, while DOMAIN matches only the exact hostname. Do not use a very broad DOMAIN-KEYWORD,docker rule unless you have audited its scope; it can capture unrelated internal services whose names happen to contain the same text.

When using mihomo, a domain-based rule can be matched before DNS returns an address. For IP-based rules, ensure the DNS behavior is consistent with the rest of the configuration. Fake-IP, redir-host, TUN DNS hijacking, and the daemon's own resolver can produce different logs. A Docker pull that reaches a registry by IP may not match a domain rule if the relevant hostname was not preserved or sniffed, so inspect Clash access logs while reproducing the failure.

Do not copy a CDN IP into permanent rules

Registry layer addresses can change by region and by request. Hard-coding one observed CDN address may make today's pull work and tomorrow's pull fail. Prefer stable domain rules, supported rule providers, or a controlled registry mirror. Use an IP rule temporarily for diagnosis, not as the main long-term design.

DNS, TLS, and Authentication Checks

Docker registry failures can look like proxy failures even when the proxy tunnel is healthy. The daemon must resolve the proxy host, establish a CONNECT tunnel for HTTPS, complete TLS validation, and follow the registry's authentication flow. If DNS returns a poisoned address, if the system clock is wrong, or if a middlebox replaces certificates, the pull can stop after the first request.

  • Check the proxy listener. Confirm that the port is open from the daemon's network namespace and that Clash logs the incoming connection.
  • Check the hostname. Resolve the proxy host and registry names using the resolver available to the Docker service, not only the resolver used by an interactive shell.
  • Check TLS time. A host clock several minutes out of sync can invalidate certificates and bearer-token signatures.
  • Check authentication separately. A 401 challenge from the registry is different from a timeout while obtaining the token.
  • Check the selected node. A node may open ordinary websites but fail on long-lived HTTPS downloads, large responses, or the registry's destination region.

Proxy Running Containers and Docker Builds

Daemon proxy settings control image distribution, not every request made inside a container. If a container installs packages during startup, calls an external API, or downloads language dependencies, it needs its own proxy variables or a transparent network path. These are separate concerns and should be documented separately in Compose files, deployment manifests, or CI configuration.

For a single container, pass proxy variables explicitly and include the local networks in NO_PROXY:

docker run --rm \
  -e HTTP_PROXY=http://host.docker.internal:7890 \
  -e HTTPS_PROXY=http://host.docker.internal:7890 \
  -e NO_PROXY=localhost,127.0.0.1,.internal \
  alpine:3.20 \
  sh -c 'apk add --no-cache curl && curl -I https://example.com'

host.docker.internal is available by default in Docker Desktop. On Linux, its availability depends on the Docker version and configuration. If it does not resolve, add a host gateway mapping or use the host's reachable bridge address:

docker run --rm \
  --add-host=host.docker.internal:host-gateway \
  -e HTTP_PROXY=http://host.docker.internal:7890 \
  -e HTTPS_PROXY=http://host.docker.internal:7890 \
  alpine:3.20 env

For Compose, keep proxy values in an environment file rather than committing credentials or private subscription details into the project repository:

services:
  worker:
    image: example/worker:latest
    environment:
      HTTP_PROXY: ${HTTP_PROXY}
      HTTPS_PROXY: ${HTTPS_PROXY}
      NO_PROXY: localhost,127.0.0.1,db,redis,.internal
    extra_hosts:
      - "host.docker.internal:host-gateway"

Build arguments are needed when a Dockerfile downloads packages during docker build. They are not automatically inherited from the daemon proxy configuration. A practical Dockerfile pattern is:

ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY

ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV NO_PROXY=${NO_PROXY}

RUN apk add --no-cache curl

Then supply the values only for the build invocation or through the CI runner's protected variables:

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 example/worker:ci .

Be careful with secrets in build arguments. Proxy URLs containing usernames or passwords can appear in build history, logs, or cache metadata. Use a proxy that does not require embedded credentials for local development, or use the build system's secret and network features where available. A proxy for package downloads should not be confused with Docker registry authentication; keep registry credentials in the credential mechanism intended for the CI runner.

Make Image Pulls Faster and More Reliable in CI

CI performance depends on more than raw node speed. Every job may repeat DNS resolution, TLS setup, registry authentication, and layer downloads. The largest gains usually come from reducing repeated work and selecting a stable route. Use a dedicated Clash proxy group for Docker traffic so a general browser rule change does not unexpectedly move build traffic to a congested node.

  • Prefer a nearby egress region. Choose a node with stable latency to the registry and content network rather than selecting the node with the lowest ping to a generic test address.
  • Keep the daemon warm. A persistent self-hosted runner can reuse local layers, while short-lived runners should use a registry mirror or a cache registry where the platform supports it.
  • Limit parallel pulls. Several jobs downloading large layers at once can exhaust the proxy node's bandwidth or connection quota. Tune runner concurrency and Docker's download settings together.
  • Use immutable image references. Tags can move between builds. Digests make cache behavior and rollback results easier to reproduce.
  • Separate registry traffic from package traffic. Docker Hub access, operating-system mirrors, and language package registries may perform better through different proxy groups.

A generic CI job can export the proxy only for commands that need it:

export HTTP_PROXY="${CI_HTTP_PROXY}"
export HTTPS_PROXY="${CI_HTTPS_PROXY}"
export NO_PROXY="localhost,127.0.0.1,docker,.internal"

docker login --username "$REGISTRY_USER" --password-stdin <<< "$REGISTRY_TOKEN"
docker pull example/app:build-${CI_COMMIT_SHA}
docker build \
  --build-arg HTTP_PROXY="$HTTP_PROXY" \
  --build-arg HTTPS_PROXY="$HTTPS_PROXY" \
  --build-arg NO_PROXY="$NO_PROXY" \
  -t example/app:"$CI_COMMIT_SHA" .

Do not print these variables in a diagnostic step if they contain credentials. In hosted CI, configure them as masked and protected variables, and avoid putting a subscription URL or authenticated proxy URL into an image layer. If the runner uses Docker-in-Docker, identify which daemon receives the variables: exporting them in the job container does not necessarily configure the sibling or service Docker daemon that performs the pull.

Measure before and after

Record the time for DNS lookup, registry authentication, manifest retrieval, and layer download separately. A faster node that has high packet loss may produce a worse total pull time than a slightly slower but stable node. Compare repeated pulls of the same image, not one successful run against one failed run.

When HTTP Proxy Variables Are Not Enough

Some workloads do not honor HTTP_PROXY and HTTPS_PROXY, and some Docker networking paths bypass application-level proxy settings entirely. This is where mihomo TUN mode or another transparent redirection design becomes relevant. TUN captures traffic at the network layer, allowing applications that know nothing about proxies to enter Clash's routing engine. It can also capture DNS when dns-hijack and the platform's route settings are configured correctly.

Transparent mode is more complex than setting a proxy variable. The TUN interface needs permission, automatic routes must not create a loop, the correct outbound interface must be selected, and private Docker bridge ranges should normally remain direct. A typical mihomo section may look like this, but field support and defaults depend on the client and kernel version:

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

Do not enable TUN and proxy environment variables blindly in the same container network. The environment variables may send traffic into Clash while TUN captures the same connection again, creating confusing loops or duplicate log entries. Choose one primary path for each traffic source, then add exceptions deliberately. For Docker bridges, preserve access to 172.16.0.0/12, 192.168.0.0/16, and any custom bridge subnet used by databases or service discovery.

For a container that must reach the host-side Clash listener, transparent capture does not automatically make 127.0.0.1 valid inside the container. Use a host gateway address, a correctly routed host interface, or a network mode designed for the platform. On Linux, network_mode: host can simplify access to a host listener, but it removes network isolation and is not an appropriate default for every workload.

Troubleshoot Failed Pulls in a Fixed Order

Change one layer at a time. Testing all settings simultaneously makes it impossible to tell whether the failure came from DNS, the proxy listener, Clash rules, Docker service configuration, or registry authentication.

  1. Confirm Clash is running and the intended proxy group has a usable node. Test the listener locally with curl -x.
  2. Confirm Docker's effective environment with systemctl show docker --property=Environment, or inspect the Docker Desktop proxy page when using Desktop.
  3. Restart the daemon after changing its configuration, then retry a small public image such as hello-world.
  4. Watch Clash access logs and Docker logs at the same time. Identify the first hostname that fails instead of focusing only on the final Docker error.
  5. Test the registry challenge with curl -x http://127.0.0.1:7890 -I https://registry-1.docker.io/v2/. A 401 response is expected for an unauthenticated request.
  6. Check whether the failure occurs during authentication, manifest retrieval, or layer download. Different stages may use different domains.
  7. Temporarily switch the Docker Clash group to another known-good node and compare a repeated pull. If only one node fails, avoid changing Docker configuration.
Observed errorLikely causeNext check
proxyconnect tcp: connection refusedWrong listener address or portCheck Clash bind address and test with curl -x
context deadline exceededUnreachable route, overloaded node, or blocked registry pathInspect Clash logs and compare another node
no such hostDNS failure for the proxy or registry hostnameCheck daemon resolver and Clash DNS behavior
401 UnauthorizedRegistry authentication is requiredRun docker login and check token-service access
x509: certificate signed by unknown authorityCertificate interception or incomplete trust configurationCheck system time, proxy inspection, and trusted CA policy
Manifest succeeds but layers stallLayer CDN route or long-download stability problemInspect later hostnames and test a different egress node

Once the pull works, remove temporary debug rules, broad bypasses, and unnecessary proxy variables. Keep the final design documented: which process uses the daemon proxy, which containers receive environment variables, which private ranges are in NO_PROXY, and which Clash group handles registry traffic. That small record prevents a future node change or CI runner migration from turning a working setup into an unexplained timeout.

Continue With Client and Configuration Setup

Use a maintained Clash client with the mihomo kernel, verify its HTTP or mixed listener, and apply the Docker rules only after confirming the network path. The download center provides current client options, while the quick-start guide covers importing a profile, selecting a mode, and checking the first connection.

Download Clash