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 source | Typical configuration point | What it affects |
|---|---|---|
| Docker CLI on the host | Shell variables or Docker client environment | CLI requests and commands that contact a registry directly |
| Docker daemon | systemd drop-in, Docker Desktop settings, or daemon environment | Image pulls, pushes, registry authentication, and layer downloads |
| Build process | Build arguments and BuildKit settings | Package downloads and remote requests made during docker build |
| Running container | Container environment or Compose configuration | Outbound traffic generated after the container starts |
| Transparent network path | Clash TUN or redirection with correct routes | Traffic 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
401challenge 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.
- Confirm Clash is running and the intended proxy group has a usable node. Test the listener locally with
curl -x. - Confirm Docker's effective environment with
systemctl show docker --property=Environment, or inspect the Docker Desktop proxy page when using Desktop. - Restart the daemon after changing its configuration, then retry a small public image such as
hello-world. - 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.
- Test the registry challenge with
curl -x http://127.0.0.1:7890 -I https://registry-1.docker.io/v2/. A401response is expected for an unauthenticated request. - Check whether the failure occurs during authentication, manifest retrieval, or layer download. Different stages may use different domains.
- 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 error | Likely cause | Next check |
|---|---|---|
proxyconnect tcp: connection refused | Wrong listener address or port | Check Clash bind address and test with curl -x |
context deadline exceeded | Unreachable route, overloaded node, or blocked registry path | Inspect Clash logs and compare another node |
no such host | DNS failure for the proxy or registry hostname | Check daemon resolver and Clash DNS behavior |
401 Unauthorized | Registry authentication is required | Run docker login and check token-service access |
x509: certificate signed by unknown authority | Certificate interception or incomplete trust configuration | Check system time, proxy inspection, and trusted CA policy |
| Manifest succeeds but layers stall | Layer CDN route or long-download stability problem | Inspect 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.