Why TUN and “system proxy” do not replace terminal env vars
TUN mode is excellent when you want broad, kernel-level steering: packets enter a virtual interface, Clash applies rules, and most programs follow the routing table without knowing a proxy exists. For a conceptual tour of stacks, DNS hijacking, and when transparency helps, read the TUN deep dive. Yet many workflows still benefit from an explicit upper-layer proxy endpoint, especially when you are debugging, scripting, or working inside tools that create their own network namespaces.
Command-line utilities are inconsistent. Some respect macOS “automatic proxy configuration” or Windows WinINET settings; on Linux there is often no single OS-wide HTTP proxy at all. git can use its own http.proxy configuration. curl honors environment variables unless overridden. Container runtimes talk to registries from isolated network contexts where your interactive shell variables never appear unless you forward them. The practical takeaway is simple: if you need reproducible behavior in scripts and CI-like sessions, define a known listener on loopback and point tools at it with standard variables.
Clash mixed-port exists to reduce port sprawl. Instead of memorizing separate port (HTTP) and socks-port values, you expose one TCP port that accepts both HTTP proxy requests and SOCKS5 sessions. You still choose the URL scheme when configuring clients—http://127.0.0.1:7890 versus socks5h://127.0.0.1:7890—but the destination port stays consistent across cheat sheets and team documentation.
Enable mixed-port in Clash and pick a stable listener
In Mihomo-class cores (often labeled Clash Meta), the listener section of config.yaml typically includes mixed-port alongside legacy port and socks-port keys. A common convention on consumer setups is 7890 for the mixed listener, but any free high port works as long as nothing else binds it and your firewall allows loopback access.
If you manage configuration by GUI, locate the equivalent toggle that exposes a “mixed” or “system proxy” port and note the numeric value. Keep the port stable across reboots; changing it weekly breaks shell profiles, IDE settings, and daemon.json snippets your teammates paste from chat logs.
# Listener excerpt only — do not copy unrelated keys blindly
mixed-port: 7890
bind-address: '127.0.0.1'
Binding explicitly to 127.0.0.1 avoids accidental LAN exposure of an open proxy on untrusted networks. If you intentionally share the proxy on a home lab, treat that as a deliberate firewall and authentication project, not a forgotten default.
After editing YAML, reload the running core (or restart the desktop client) and confirm the port is listening with ss -lntp or lsof -iTCP:7890 -sTCP:LISTEN on Linux, or lsof on macOS. No listener means every subsequent environment variable line is harmless but useless.
HTTP proxy URL versus socks5h:// for curl and git
The mixed port accepts both protocols, but clients speak one dialect at a time. For tools that understand classic HTTP CONNECT tunneling, an http:// URL is enough. For programs that want arbitrary TCP over SOCKS, use socks5:// or socks5h://. The trailing h variant asks cURL-like stacks to resolve hostnames remotely through the proxy (“proxy resolves DNS”), which matters when local DNS is filtered or when you want the same view of names as the proxy’s rule engine.
git over HTTPS typically works well with an HTTP proxy URL. Some users prefer SOCKS for mixed transports; either can work if your proxy implements the relevant RFC behavior cleanly. When in doubt, test git ls-remote against a known HTTPS remote after exporting variables in a fresh shell.
Authentication is a separate concern. Home Clash instances on loopback usually run without per-request proxy authentication, which keeps developer ergonomics high. Corporate upstreams may still require credentials at a different layer; this article assumes a local Clash hop you control, then normal node selection inside YAML.
HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and lowercase twins
Most Unix programs that respect proxies look for a combination of uppercase and lowercase variable names. Defining both reduces “works on my laptop” variance when a tool only implements one spelling.
HTTP_PROXY/http_proxy— used forhttp://URLs in many clients.HTTPS_PROXY/https_proxy— used forhttps://traffic in stacks that split schemes.ALL_PROXY/all_proxy— fallback for tools that route “everything else” through one URL, including some SOCKS-friendly code paths.FTP_PROXY— rarely needed in 2026, but legacy scripts still exist; leave unset unless you know you need it.
A pragmatic starting point on loopback with mixed-port 7890 is to align HTTP variables on the HTTP scheme and put SOCKS in ALL_PROXY when a tool prefers it. Some teams standardize entirely on socks5h://127.0.0.1:7890 for ALL_PROXY because it covers more exotic TCP clients; others standardize on HTTP because corporate inspection tooling understands it better. Pick one team convention and document it.
export http_proxy="http://127.0.0.1:7890"
export https_proxy="http://127.0.0.1:7890"
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export all_proxy="socks5h://127.0.0.1:7890"
export ALL_PROXY="$all_proxy"
Place those lines in ~/.zshrc or ~/.bashrc if you want persistence, but consider wrapping them in a function such as proxy_on / proxy_off so domestic-only workflows do not accidentally send sensitive internal URLs through a remote exit. Toggle discipline beats chasing mysterious leaks six months later.
Designing NO_PROXY so localhost and registries stay sane
NO_PROXY (and no_proxy) lists hosts that should bypass the proxy and use direct sockets. This is where many setups catch fire: forgetting loopback causes tools to try to proxy traffic that was never meant to leave the machine; omitting internal registry hostnames breaks docker pull from a private Harbor instance sitting on RFC1918 space.
Common entries include:
- Loopback —
localhost,127.0.0.1, and sometimes::1. - Local domain sockets and metadata — cloud provider metadata IPs if you intentionally avoid proxying them (policy-dependent).
- Private networks — comma-separated suffix entries like
.corp.exampleor CIDR-style lists if your toolchain supports them. - Docker Desktop host alias — on macOS and Windows,
host.docker.internalappears in many pull-through cache examples; add it when containers must reach a service published on the host without detouring through an egress node.
export no_proxy="localhost,127.0.0.1,::1,host.docker.internal,.internal,.lan"
export NO_PROXY="$no_proxy"
Matching rules differ slightly across libraries. Some implementations honor leading-dot suffixes; others require explicit hostnames. When something “should be DIRECT but is not,” reproduce with curl -v and read whether a proxy handshake occurs; then tighten NO_PROXY iteratively instead of guessing ten entries at once.
Verify from the shell before you touch Docker
Start with observability, not hope. After exporting variables, run:
curl -I https://www.example.com— confirms TLS + HTTP proxy path (replace with a host you are allowed to test).git ls-remote https://github.com/...— validates git’s HTTP stack through the same env.npm ping,go env -wexperiments, or language-specific probes — only after the basics work.
If these fail while the Clash dashboard shows no corresponding connection, your variables are not active in that shell, the port is wrong, or a local firewall blocks loopback forwarding. Fix the shell first: Docker will not magically inherit fixes you never made to the parent environment of dockerd.
For policy ordering and DNS interactions once traffic actually reaches Clash, cross-check routing rules best practices and the DNS notes in the FAQ—explicit proxying does not excuse contradictory fake-ip setups.
Docker Engine: daemon.json proxies for pull and push
The Docker daemon that performs registry pulls is not your interactive shell. Exporting HTTP_PROXY in ~/.zshrc does not change how dockerd fetches images unless the daemon process was started with those variables or configured via file. The maintainable pattern on Linux and Docker Desktop’s engine is the proxies block inside daemon.json.
Under proxies.default, set httpProxy and httpsProxy to your loopback mixed listener using an http:// URL, then enumerate bypasses in noProxy. Keep JSON valid—trailing commas break the daemon start and waste an afternoon.
{
"proxies": {
"default": {
"httpProxy": "http://127.0.0.1:7890",
"httpsProxy": "http://127.0.0.1:7890",
"noProxy": "localhost,127.0.0.1,::1,host.docker.internal"
}
}
}
Restart Docker after changes. Confirm pulls work against both a public registry and any internal mirror you rely on. If internal pulls break, your noProxy list is incomplete—add explicit registry hostnames or domain suffixes for your org.
daemon.json may be world-readable depending on OS defaults. Avoid embedding secrets there; the local Clash hop should remain unauthenticated on loopback, while upstream credentials belong in your subscription or node configuration inside Clash itself—not in Docker’s JSON.
docker build, BuildKit, and build-time proxy arguments
Image builds often download Debian or Alpine packages, git clone dependencies, or language artifacts. Those steps run inside ephemeral build containers. Docker BuildKit forwards certain proxy environment variables from the client CLI when you pass --build-arg lines, enabling RUN curl or apk add to see the same egress path.
Typical arguments include HTTP_PROXY, HTTPS_PROXY, NO_PROXY, and lowercase variants. Align their values with what you proved in the host shell. Remember that build stages can call internal artifact servers: extend NO_PROXY inside build args to cover those hostnames, or builds will time out while trying to proxy RFC1918 addresses through a public node group.
docker build \
--build-arg HTTP_PROXY=http://127.0.0.1:7890 \
--build-arg HTTPS_PROXY=http://127.0.0.1:7890 \
--build-arg NO_PROXY=localhost,127.0.0.1,::1 \
-t myapp:local .
On Docker Desktop for macOS, loopback inside the Linux VM is not always identical to the Mac’s 127.0.0.1. Many users route build traffic through host.docker.internal instead, pointing proxy variables at http://host.docker.internal:7890 so the VM reaches the host-published Clash port. Test both mental models; pick the one that matches your Desktop version and network panel.
Compose users can centralize args under build.args or environment blocks per service; the underlying idea stays the same—explicit, repeatable proxy endpoints per build graph edge.
Docker Desktop, host.docker.internal, and Linux bridge notes
On macOS and Windows, Docker runs a lightweight Linux VM. Your Clash process listens on the host loopback interface; containers reach it through special DNS names bridged by Desktop. That is why community snippets often show host.docker.internal:7890 instead of raw 127.0.0.1 during builds.
On native Linux Docker without Desktop, containers share the host kernel but use separate network namespaces. 127.0.0.1 inside a container refers to the container itself, not the host’s Clash listener. Common fixes include advertising the docker bridge gateway IP (often 172.17.0.1), binding Clash on 0.0.0.0 with firewall rules (carefully), or running a sidecar proxy on an attachable user-defined network. Document whichever approach you choose; newcomers will copy it verbatim at 2 a.m.
Kubernetes and nerdctl users face the same namespace lesson under different flags. The pattern is always: identify which network namespace initiates the outbound TCP connection, then choose an address that namespace can route to.
Troubleshooting: loops, DNS, and “still DIRECT” mysteries
Symptom: enabling variables breaks a site that used to work. First check whether that hostname belongs in NO_PROXY. Second, read Clash logs to see if a rule sends the flow to an unexpected policy group—proxying alone does not fix bad MATCH defaults.
Symptom: infinite loops or hang on connect. Usually the proxy port is unreachable from the namespace you configured, or you pointed Docker at 127.0.0.1 inside a container where loopback is wrong. Replace with the correct bridge or host alias.
Symptom: only HTTPS works, HTTP does not (or vice versa). Verify both http_proxy and https_proxy. Some minimal containers only ship one library path.
Symptom: TUN on but CLI still “DIRECT” in Clash UI. That can be correct: TUN intercepts IP packets, while env-var proxying creates a separate explicit path. Use connection rows to learn which mechanism handled a flow instead of assuming redundancy implies failure.
Wrap-up
Clash mixed-port gives you a single, memorable local endpoint for both HTTP proxying and SOCKS5. Pair it with disciplined HTTP_PROXY / HTTPS_PROXY / ALL_PROXY exports and a thoughtful NO_PROXY list, and your terminals gain predictable egress without waiting for every app to honor OS proxy panes. Move the same ideas into daemon.json and build arguments, and Docker stops being the odd tool that “ignored Clash.”
Compared with chasing per-tool toggles, explicit environment configuration is more portable across shells, IDEs, and CI jobs. It also complements—not replaces—TUN when you need whole-system steering for apps that will never read proxy variables. Together, the two layers cover most real desktops and small servers developers actually run.
When you want a polished client that exposes mixed listeners, live connections, and readable rules without forcing you to memorize every YAML edge case, modern Clash-based apps reduce the operational tax of staying connected. The engineering trade-off is familiar: a few minutes wiring env vars and Docker JSON today saves hours of unexplained timeouts tomorrow.
→ Download Clash for free and experience the difference—then wire mixed-port, paste your proxy exports, and prove the path with curl before you trust a long docker build.