Docker has three proxy settings, and each one covers different traffic. The daemon's setting covers image pulls and pushes. The client's setting covers containers and builds. Docker Desktop has a third, and it ignores the daemon's file. Most proxy trouble comes from setting one of them and expecting another to follow.
The errors follow the same split. Every proxyconnect tcp error comes from the daemon on its way to a proxy, and the words after that label name the failure. We read Docker's documentation and source code, counted the errors on Docker's GitHub, and ran Go's HTTP library against a stub proxy on our own machine.
Daemon (dockerd)
pull, push, login: daemon.json or a systemd drop-in
Containers and builds
RUN steps and running apps: ~/.docker/config.json
Docker Desktop
its own Proxies settings; daemon.json proxies ignored
Which Docker proxy setting does your command use?
| What fails | Which setting | Where it lives |
|---|---|---|
docker pull, docker push, docker login | The daemon | daemon.json or a systemd drop-in |
A RUN step in docker build | The build | ~/.docker/config.json or --build-arg |
| An app inside a running container | The container | ~/.docker/config.json or --env |
| Anything on Docker Desktop for Windows or Mac | Desktop | Settings, Resources, Proxies |
Read the failing command first, then fix only that setting.
How do you set the daemon's proxy?
The daemon makes the registry connections for pulls and pushes. Docker's documentation recommends daemon.json:
{
"proxies": {
"http-proxy": "http://proxy.example.com:3128",
"https-proxy": "http://proxy.example.com:3128",
"no-proxy": "localhost,127.0.0.0/8,registry.internal.example.com,.corp"
}
}
Then restart it with sudo systemctl restart docker. On a systemd host, a drop-in file works too, at /etc/systemd/system/docker.service.d/http-proxy.conf:
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,registry.internal.example.com,.corp"
A drop-in needs sudo systemctl daemon-reload before the restart, or the old values survive. Four details from the documentation save time:
- The file wins. "Configuring the daemon directly takes precedence over environment variables."
- Special characters need escaping. In the drop-in, characters such as
#?!()[]{}"must be double escaped using %%". - Rootless Docker reads another place. Its drop-in sits in
~/.config/systemd/user/docker.service.d/, andsystemctlruns with--userand withoutsudo. - The exclusion list has rules.
example.comcovers the domain and its subdomains,.example.comcovers subdomains only, and a single*turns the proxy off for everything.
Check what the daemon believes with sudo systemctl show --property=Environment docker, and with docker info, which prints the proxy values in use.
How do containers and builds get a proxy?
Nothing inside a container inherits the daemon's proxy. A RUN apt-get update in a build, or pip install in a running container, opens its own connections. The client hands them a proxy from ~/.docker/config.json:
{
"proxies": {
"default": {
"httpProxy": "http://proxy.example.com:3128",
"httpsProxy": "http://proxy.example.com:3128",
"noProxy": "localhost,127.0.0.1,.corp"
}
}
}
The file needs no restart. By Docker's documentation, it applies to new containers and builds only, and leaves running containers alone. For one run, pass the values by hand: --env HTTPS_PROXY=... on docker run and --build-arg HTTPS_PROXY=... on docker build.
Two warnings from the same documentation matter for anyone with a password in the proxy address:
- Never use
ENVfor a build proxy. "Don't use the ENV Dockerfile instruction to specify proxy settings for builds. Use build arguments instead."ENVwrites the proxy into the image, and every container from it inherits the address. - Container variables are readable. They "are stored as plain text in the container's configuration", so anyone with access to the Docker API can read them.
Compose files take the same variables under environment: or from an env_file:, and under build: args: for builds. Add your service names, such as db or redis, to NO_PROXY. Otherwise one container asks the proxy for another container it could reach directly.
How does Docker Desktop handle proxies?
Docker Desktop ignores the proxy keys in daemon.json, as the daemon documentation says plainly. It takes the proxy from Settings, Resources, Proxies, or from the system's own settings. Three points from Docker's Desktop documentation:
- PAC files must cover registries. A PAC file has to return a proxy for Docker's registry addresses, "or image pulls will fail".
- WPAD is not supported. Automatic discovery "is untested on both Windows and Mac", so enter a PAC address or a proxy by hand.
- Containers have their own switch. They can follow the host, use the system proxy, use none, or take a manual setting.
What does proxyconnect tcp mean?
Docker's daemon reaches registries through Go's standard HTTP library, with the proxy taken from its environment. When a proxy is set, Go wraps every error on the way to it in one label. Its source says so in a line: return &net.OpError{Op: "proxyconnect", Net: "tcp", Err: err}.
So proxyconnect tcp tells you the failure happened between the daemon and the proxy, before any registry was involved. The words after it tell you what failed. We counted them in the issues on Docker's GitHub repositories:
- connection refused: nothing listens at that address and port. The port is wrong, the proxy is down, or the host is on a network without it.
- i/o timeout: no answer came back. Docker's registry client gives a connection 30 seconds, by its source code. A firewall or a wrong address usually explains the silence.
- no such host: the daemon cannot resolve the proxy's own name. Fix DNS on the host, or use the proxy's address.
- EOF: the proxy closed the connection without an answer. Check that the port speaks the protocol in the address, HTTP or SOCKS5.
- first record does not look like a TLS handshake: the address says
https://and the proxy speaks plain HTTP. The next section shows it happen.
Two failures carry no proxyconnect label, because the connection to the proxy worked. A 407 Proxy Authentication Required means the proxy wants a login (11 issues on Docker's GitHub quote it). A bare Forbidden means the proxy refused the tunnel.
Why does an https:// proxy address break the pull?
In Go, the proxy address decides how the first hop starts. With http://, Go sends a plain CONNECT line to the proxy. With https://, Go opens a TLS handshake with the proxy itself before anything else.
We wrote a small Go program to watch both. Its stub "proxy" listens on 127.0.0.1, logs the first bytes it receives and refuses every request, so nothing leaves the machine. We ran it on Ubuntu 24.04 under WSL2:
The bytes 16 03 01 open a TLS record of type handshake. A plain HTTP proxy cannot read them, so it answers with an HTTP error in plain text. Go reads that answer as its first TLS record and stops. The check in Go's TLS code is short: anything other than a handshake or an alert "does not look like a TLS handshake".
The fix is the scheme. Write http:// for a proxy that speaks plain HTTP, which is how the examples in Docker's documentation write the daemon's proxy. Keep https:// only for a proxy that really speaks TLS on that port. Docker Desktop, for one, recommends an https:// proxy address to protect passwords in transit, where the proxy supports it.
What do TLS handshake timeout and i/o timeout mean?
Both mean that no answer arrived in time. Docker's registry client waits 30 seconds for a connection and 10 seconds for a TLS handshake, by its source code.
Without the proxyconnect label, the daemon was talking to the registry directly. On a network that only lets traffic out through a proxy, that is the error you see before you set one. With the label, the proxy itself stalled. Test it from the host:
curl -v -x http://proxy.example.com:3128 https://registry-1.docker.io/v2/
A timeout is not a certificate problem. An untrusted certificate fails at once, with the x509 error below.
How do you fix x509: certificate signed by unknown authority?
Some corporate proxies inspect HTTPS. They sign each site's certificate with the organization's own certificate authority, which the Docker host does not trust. Install that authority on the host:
- Debian and Ubuntu: copy the root certificate to
/usr/local/share/ca-certificates/and runsudo update-ca-certificates. - RHEL and Fedora: copy it to
/etc/pki/ca-trust/source/anchors/and runsudo update-ca-trust. - One registry only: Docker also reads
ca.crtfrom/etc/docker/certs.d/<registry-hostname>/. On Linux, those roots are added to the system's own set.
Restart Docker afterwards. Inside a build, the container has its own trust store, so copy the certificate into the image and run the same update command there. Node.js and Python tools may also need NODE_EXTRA_CA_CERTS or REQUESTS_CA_BUNDLE pointed at it.
The wrong fix is an insecure registry in daemon.json. That turns verification off instead of trusting the right authority.
Can Docker pull images through a SOCKS5 proxy?
By Go's source, yes. The proxy code in Go's HTTP library handles socks5:// and socks5h:// addresses next to http:// and https://. A SOCKS5 address in the daemon's proxy settings therefore reaches that code. We read the code and did not run Docker with one, so test it on your own setup first. Our Linux proxy guide covers what other tools do with the same variables.
Where does a residential proxy fit?
At the container layer only. A scraper or checker in a container sometimes needs to reach sites from a home address, and the HTTP_PROXY variables carry that. Keep the login out of the image and pass it at run time from an env file:
# proxy.env: Lite gateway, HTTP port; use the login your dashboard shows
HTTPS_PROXY=http://YOUR-LOGIN:YOUR-PASSWORD@gw.hproxy.com:6969
HTTP_PROXY=http://YOUR-LOGIN:YOUR-PASSWORD@gw.hproxy.com:6969
NO_PROXY=localhost,127.0.0.1,db,redis
docker run --env-file proxy.env my-scraper
On Premium, premium.hproxy.com takes port 9000 for a rotating address and ports from 10000 up for sticky sessions. Our FlareSolverr guide covers a common trap here: inside a container, 127.0.0.1 is the container itself, not the host.
Keep image pulls off any per-GB proxy. Images are big:
A CI runner that starts empty pulls its base image on every run. A hundred fresh pulls of python:3.12 move about 41 GB. Through a proxy billed at $0.50/GB, that would cost $20.50 for traffic that gains nothing from a home address. Leave the daemon direct, or on your company's proxy, and let only the scraper's own requests use the gateway. Residential Lite costs $0.50/GB for one GB, falling to $0.44/GB at 2,000 GB+.
A checklist that ends most cases
- Name the setting. Pulls and pushes are the daemon,
RUNsteps are the build, apps are the container, Windows and Mac are Desktop. - Print what it believes. Use
systemctl show --property=Environment dockeranddocker infofor the daemon, anddocker run --rm alpine env | grep -i proxyfor containers. - Read the words after proxyconnect. Refused means the address or port, timeout means silence, no such host means DNS.
- Match the scheme to the proxy. Write
http://for a plain HTTP proxy. - Reload, then restart. A changed drop-in needs
daemon-reloadbeforerestart. - Trust the authority. For x509, install the certificate and restart, and mark nothing insecure.
What this page could not check
- We did not run Docker itself. Changing the daemon's proxy would have changed a working setup on our machine. The errors come from Go's HTTP library, which Docker's registry client uses, driven by our own program.
- No real proxy was tested, corporate or commercial, ours included. The stub on 127.0.0.1 forwarded nothing.
- The GitHub counts are text matches in issues on seven Docker repositories. One issue can match several errors, and most failures never reach GitHub.
- Desktop is described from its documentation. We did not open its settings.
- Releases change things. Docker's documentation, Go's wording and image sizes move with new versions; we will check this page again by January 23, 2027.
Sources
- Docker Docs, Daemon proxy configuration, modified April 17, 2026, read October 22, 2026.
- Docker Docs, Use a proxy server with the Docker CLI, modified November 5, 2024, read October 22, 2026.
- Docker Docs, Docker Desktop settings, modified September 29, 2026, read October 22, 2026.
- Docker Docs, Verify repository client with certificates, modified August 11, 2026, read October 22, 2026.
- Go source at go1.25.0 (August 12, 2025): net/http/transport.go and crypto/tls/conn.go.
- Moby source, daemon/pkg/registry/registry.go, read October 23, 2026.
- Docker Hub API, compressed sizes of python:3.12, node:22, ubuntu:24.04 and alpine:3.20, read October 23, 2026.
- HProxy count of GitHub issues in moby/moby, docker/for-mac, docker/for-win, docker/for-linux, docker/cli, docker/compose and docker/buildx, October 22, 2026.
- HProxy test run: our own Go program against a stub proxy on 127.0.0.1, Ubuntu 24.04 on WSL2, October 23, 2026.



