Why Docker Desktop on macOS ignores your Clash system proxy

Docker Desktop is not a thin wrapper around native macOS processes. The engine you invoke from Terminal talks to a virtualized Linux environment that downloads images, runs containers, and executes RUN steps during builds. That VM has its own routing table and DNS view. When Clash Verge Rev or another Mihomo-based client enables a local HTTP or SOCKS listener on the Mac, only userspace programs on macOS automatically benefit from tray shortcuts or system proxy toggles. Packet flows that originate inside the Docker VM must explicitly target a host address where your proxy listens.

The stable hostname Docker documents for this hop is host.docker.internal. It resolves from Linux containers to an IP that reaches the Mac host, which is exactly where Clash binds its mixed or HTTP port. Once you combine that hostname with standard http_proxy / https_proxy environment variables—or Docker’s own proxy configuration—you give registries, Debian mirrors, and language package managers the same exit path your browser already uses.

If you are new to listener layout on Apple Silicon or Intel Macs, walk through the Clash Verge Rev setup guide first so you know the real port numbers in your YAML. The examples below assume a mixed port of 7890; substitute yours everywhere.

Windows developers who forward WSL2 through host Clash should keep using our WSL2 plus Docker networking guide. The mental model overlaps, but macOS never exposes the same /etc/resolv.conf gateway trick, so the hostname-based recipe here is the portable fix.

Use host.docker.internal as the proxy host inside containers

From any container on Docker Desktop for Mac, ping or curl the special DNS name host.docker.internal. Docker injects it so guest Linux can reach services published on the Mac loopback interface. That is where most Clash builds listen when you start the local mixed or HTTP inbound.

Your proxy URL therefore looks like http://host.docker.internal:7890 for HTTP CONNECT–capable clients. Tools that insist on SOCKS can point at socks5://host.docker.internal:7891 only if you actually expose SOCKS on that port; many profiles collapse both protocols onto a single mixed listener, in which case stay on the HTTP form for simplicity.

Avoid hard-coding raw gateway IPs. Docker may adjust internal bridging after updates, while host.docker.internal tracks the correct attachment point. The same hostname works for both aarch64 and x86_64 Macs, which saves you from maintaining two compose files.

Prepare Clash on the Mac: bind address, allow LAN, and firewall

Many default profiles bind inbound proxies to 127.0.0.1 only. Traffic from the Docker VM arrives as a remote peer, not as localhost on macOS, so Clash must accept connections on a broader interface. In YAML this corresponds to allow-lan: true and setting the mixed or HTTP listen address to 0.0.0.0 for the relevant port. GUI clients expose the same idea as an “Allow LAN” toggle next to the port field.

macOS Application Firewall prompts can block first-time listeners. If curl from a container hangs while curl from macOS succeeds, open System Settings → Network → Firewall Options and ensure your Clash client is allowed to accept incoming connections. Corporate MDM profiles occasionally enforce stricter rules; in that case coordinate with IT or temporarily test on a personal machine.

For a deeper discussion of sharing the proxy with other devices on the same Wi-Fi, see Enable Clash LAN proxy on Windows and macOS. The security trade-offs are identical when the “remote” client is actually Docker instead of an iPad.

Sanity check from macOS before you enter a container:

curl -I --proxy "http://127.0.0.1:7890" https://registry-1.docker.io/v2/

Swap the port to match your profile. You should see HTTP response headers instead of immediate connection failures.

Quick in-container test with docker run

Launch an ephemeral Alpine or Debian container with proxy variables injected at runtime:

docker run --rm -it \
  -e HTTP_PROXY="http://host.docker.internal:7890" \
  -e HTTPS_PROXY="http://host.docker.internal:7890" \
  -e NO_PROXY="localhost,127.0.0.1,::1" \
  alpine:3.20 sh -c "wget -S -O- https://example.com | head -n 5"

If wget returns HTML, the datapath from container to host proxy works. Failures usually mean the listener is still loopback-only, the port is wrong, or a firewall rule dropped the SYN packet.

Uppercase HTTP_PROXY and lowercase http_proxy are both respected by most tools, but mixing them inconsistently confuses Node and Go binaries. Export both pairs when you want maximum compatibility:

-e HTTP_PROXY="http://host.docker.internal:7890" \
  -e http_proxy="http://host.docker.internal:7890" \
  -e HTTPS_PROXY="http://host.docker.internal:7890" \
  -e https_proxy="http://host.docker.internal:7890"

Docker Compose: environment, build args, and profiles

Real projects rarely rely on one-off docker run flags. Compose lets you centralize proxy defaults per service and reuse them during builds.

A minimal docker-compose.yml fragment might look like this:

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,host.docker.internal,.internal

Build arguments matter because docker compose build executes Dockerfile steps in an isolated environment that does not automatically inherit your shell exports. Without ARG HTTP_PROXY lines in the Dockerfile, BuildKit cannot forward the values into RUN apt-get update. Add matching declarations immediately before the first RUN that needs the network:

ARG HTTP_PROXY
ARG HTTPS_PROXY
RUN apt-get update && apt-get install -y --no-install-recommends curl

Keep NO_PROXY broad enough to cover private artifact registries and internal Git hosts so those requests bypass Clash and stay on the corporate LAN. Extend the list with comma-separated hostnames or suffixes like .corp.example.com.

When multiple developers share the same repository, consider a .env file that is git-ignored and document the expected variable names in README. Never commit plaintext credentials into proxy URLs; Clash already handles upstream authentication for public nodes.

Docker Desktop proxy UI and daemon-level behavior

Docker Desktop exposes a graphical proxy panel under Settings → Resources → Proxies on recent releases. When you enter manual proxy URLs there, the engine applies them to daemon-driven operations such as image pulls initiated by the backend, not only to containers you start manually.

Set both HTTP and HTTPS fields to http://host.docker.internal:7890 unless your infrastructure requires distinct upstreams. Mirror the same host.docker.internal hostname rather than a LAN IP so upgrades to Docker’s internal networking do not break your setup.

After saving, restart Docker Desktop from the whale menu. Validate with docker pull alpine:3.20 while Clash logging is verbose enough to show new outbound flows. If the pull succeeds with Clash paused, you are still hitting a direct route—double-check rules and the DIRECT policy for registry endpoints.

BuildKit, multi-stage builds, and npm or pip

BuildKit is enabled by default on current Docker Desktop builds. It parallelizes layers and may spawn additional temporary containers for cache exports. Those auxiliary processes still honor HTTP_PROXY when you export the variables in the build scope, which is why duplicating them as build args remains important.

Node projects often call the registry during npm ci. Ensure the Dockerfile copies package.json before the install step and that the install step runs after proxy ARG declarations. Python wheels pulled by pip follow the same rule: without proxy variables, RUN pip install reaches out directly and stalls on restrictive networks.

Go module downloads respect HTTPS_PROXY. Rust’s cargo also honors the standard environment variables. Rather than sprinkling tool-specific configuration, normalize on the shared proxy exports at the top of the Dockerfile.

apt, yum, git, and certificate trust inside long-running containers

Debian and Ubuntu containers need /etc/apt/apt.conf.d/ snippets if you want apt to use a proxy even when shell environment variables are unset. The Acquire directives mirror what we describe for WSL2, only the host name changes:

Acquire::http::Proxy "http://host.docker.internal:7890/";
Acquire::https::Proxy "http://host.docker.internal:7890/";

Git reads http.proxy configuration or the environment. For CI images, prefer environment injection so credentials never land in git config layers unintentionally.

If you terminate TLS with a corporate middlebox, import the enterprise root into the container trust store. Otherwise Clash may forward the traffic correctly while apt still fails certificate verification. That symptom is easy to misattribute to “proxy broken” when the real issue is trust anchors.

DNS, fake-ip, and registry name resolution

Clash’s DNS section may use fake-ip or redir-host modes. Most containerized clients perform ordinary A/AAAA lookups through the embedded Docker DNS server, which forwards to upstream resolvers defined by Docker Desktop. When a name resolves to a fake-ip range that only exists inside Clash’s memory, non-tun processes on macOS can behave differently from containers.

When you see mysterious “name resolves but connection resets” errors, compare dig registry-1.docker.io on macOS versus inside a container. Aligning Docker’s DNS upstream with the resolver you trust in Clash often stabilizes the outcome. For stubborn cases, temporarily set "dns": ["8.8.8.8"] in Docker Desktop’s daemon JSON to isolate whether the clash is DNS or transport related.

General Clash rule and subscription issues belong in the Clash troubleshooting guide after you confirm plain curl --proxy tests succeed from both macOS and a container.

Troubleshooting checklist

Connection refused from the container

Clash is still bound to 127.0.0.1 only. Enable LAN access or listen on 0.0.0.0 for the mixed port, then retest.

Timeouts only while Docker pulls huge layers

Daemon-level proxy settings might be empty even though interactive shells work. Fill the Docker Desktop proxy form or configure proxies in daemon.json, then restart the backend.

Build works on host but fails inside CI Dockerfile

Missing ARG HTTP_PROXY forwarding. Add args in compose or pass --build-arg explicitly in CI.

Corporate registries break when Clash is on

Add the registry hostname to NO_PROXY and to Clash DIRECT rules so sensitive traffic avoids the public node group.

Security and least privilege

Opening the mixed port beyond loopback increases exposure whenever you join untrusted networks. Pair allow-LAN style bindings with macOS firewall rules, disable the behavior on public Wi-Fi, and never publish Clash’s external controller port to 0.0.0.0. Treat host.docker.internal as a trusted channel only while Docker runs on your personal machine.

Summary

Docker Desktop on macOS is a Linux VM in disguise, so Clash on the host is invisible until you bridge it with host.docker.internal and explicit HTTP_PROXY settings. Verify listeners with curl from macOS, then mirror the same URL in compose files, build args, and optional Docker Desktop proxy fields. Once the datapath works, image pulls, apt, npm, and git inside containers line up with the browser experience you already trust.

Compared with juggling multiple VPN clients, keeping a single Mihomo-based profile on the Mac and teaching Docker how to reach it reduces drift between host and container networking. When you want that workflow without hand-editing low-level YAML on day one, the official client streamlines downloads and updates while you focus on compose files. → Download Clash for free and experience the difference