Docker behind a proxy has one property that turns a five-minute job into an afternoon: there are three proxy settings, they live in three different places, and they cover three different kinds of traffic. The daemon has one, for pulling and pushing images. The client has one, injected into containers so that the software inside them can reach out. Docker Desktop has its own, and ignores the daemon's file. Every Docker proxy error is a message about one of the three, and the fastest path through is to read the error, identify the layer, and fix only that layer.
The configuration in this guide follows Docker's own daemon proxy documentation; the error messages are the ones the daemon and client actually print.
The three layers
Daemon (dockerd)
pull, push, login: daemon.json or systemd
Containers and builds
apt, pip, npm inside: ~/.docker/config.json
Docker Desktop
its own Proxies screen; daemon.json ignored
The daemon. dockerd makes the registry connections for docker pull, docker push, and docker login. It reads its proxy from 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"
}
}
followed by sudo systemctl restart docker. Or, on systemd hosts, from a drop-in file 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"
followed by sudo systemctl daemon-reload and sudo systemctl restart docker. Docker's documentation notes that the daemon.json values take precedence over the environment, that rootless Docker uses ~/.config/systemd/user/docker.service.d/http-proxy.conf without sudo and with --user, and that special characters in the values need %% escaping in the systemd file. Verify with sudo systemctl show --property=Environment docker, and with docker info, which prints the proxy values the daemon is using.
Containers and builds. Nothing inside a container inherits the daemon's proxy. A RUN apt-get update in a Dockerfile, or pip install inside a running container, makes its own connections and needs its own environment. The client injects it 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"
}
}
}
With that in place, docker run and docker build set HTTP_PROXY, HTTPS_PROXY, and NO_PROXY in the container automatically. The one-off equivalents are -e HTTP_PROXY=... on docker run and --build-arg HTTP_PROXY=... on docker build.
Docker Desktop. On Windows and macOS, Docker's documentation is explicit that proxy settings in daemon.json are ignored; the setting lives in Settings, Resources, Proxies, and Desktop applies it to the daemon it manages. The container layer still comes from ~/.docker/config.json.
The errors
| Error text | Layer | What happened | Fix |
|---|---|---|---|
Get "https://registry-1.docker.io/v2/": proxyconnect tcp: dial tcp 10.0.0.5:3128: connect: connection refused | Daemon | The daemon's proxy is unreachable | Fix the address; reload and restart |
proxyconnect tcp: tls: first record does not look like a TLS handshake | Daemon | https:// scheme on a plain HTTP proxy | Use http:// |
Get "https://registry-1.docker.io/v2/": net/http: TLS handshake timeout | Daemon | Direct access is blocked, or the proxy stalls | Set the daemon proxy, or fix it |
x509: certificate signed by unknown authority | Daemon or container | An inspecting proxy re-signs HTTPS | Install the corporate CA |
dial tcp: lookup registry-1.docker.io on ...: no such host | Daemon | DNS cannot resolve the registry | Fix DNS, or the proxy resolves it |
Get "https://registry-1.docker.io/v2/": ... i/o timeout | Daemon | No answer at all | Same as the handshake timeout |
apt-get, pip, or npm fails inside a build or container while docker pull works | Container | The container has no proxy | config.json proxies, or -e / --build-arg |
| A private registry fails with a proxy 403 or connection error | Either | The internal host is going through the proxy | Add it to no-proxy / NO_PROXY / noProxy |
proxyconnect tcp, connection refused
The most common one, and the clearest. proxyconnect means the daemon is trying to reach a proxy, and connection refused means nothing is listening at the address after dial tcp. Look at the address in the message: a typo, a wrong port, a proxy that is down, or a host on a network where the proxy does not exist. Check what the daemon believes with systemctl show --property=Environment docker and docker info, correct the file that holds the value, and remember that a systemd drop-in needs daemon-reload before the restart or the old value survives.
A dial tcp 127.0.0.1:... variant means the daemon was pointed at a proxy on the Docker host itself, often a leftover from a local debugging proxy, and nothing is running there now.
tls: first record does not look like a TLS handshake
The daemon's https-proxy (or HTTPS_PROXY) begins with https://. Docker took that as an instruction to encrypt the connection to the proxy itself, sent a TLS handshake, and the proxy answered in plain HTTP, which Go reports with this line. The key name is about the traffic the proxy carries; the scheme is about the proxy itself, and almost every proxy listens in plain HTTP. Change the scheme to http:// and restart.
TLS handshake timeout, and i/o timeout
The daemon reached out and nothing came back. On a network that only permits internet access through a proxy, this is the error you get before configuring one: the daemon is trying to reach the registry directly, and the network drops it. Configure the daemon proxy. With a proxy configured, the same error means the proxy is stalling; test it from the host with curl -v -x http://proxy.example.com:3128 https://registry-1.docker.io/v2/ and read the result.
x509: certificate signed by unknown authority
The proxy inspects HTTPS, so the certificate the daemon receives was issued by the organisation's own authority rather than the registry's, and the Docker host does not trust that authority. Fix it at the host level so that the daemon and everything else on the machine trust the authority: on Debian and Ubuntu, copy the root certificate to /usr/local/share/ca-certificates/corporate-root.crt and run sudo update-ca-certificates; on RHEL, CentOS, and Fedora, copy it to /etc/pki/ca-trust/source/anchors/ and run sudo update-ca-trust; then restart Docker. For a single registry, Docker also reads /etc/docker/certs.d/<registry-hostname>/ca.crt. On Docker Desktop, install the certificate in the operating system's store and restart Desktop. Inside containers and builds, the same error means the container's own trust store lacks the authority, and the fix is to copy the certificate into the image and run the distribution's update command in the Dockerfile.
The wrong fix is marking the registry insecure in daemon.json. That disables verification rather than establishing trust.
no such host
The daemon cannot resolve the registry hostname. On a network where DNS only works through the proxy, configuring the proxy fixes it, because the proxy resolves names on the daemon's behalf. Elsewhere it is the host's DNS: nslookup registry-1.docker.io fails the same way, and /etc/resolv.conf or the network settings are the fix.
Containers that cannot reach out
docker pull works, so the daemon proxy is fine, and then RUN apt-get update in a build hangs or pip install inside a container times out. This is the second layer with no proxy. Add the proxies.default block to ~/.docker/config.json, or pass --build-arg HTTP_PROXY=... --build-arg HTTPS_PROXY=... to the build and -e HTTP_PROXY=... -e HTTPS_PROXY=... to the run. Include NO_PROXY for anything on the internal network the container should reach directly. Our Linux proxy guide covers what the tools inside the container do with those variables once they arrive, including why sudo apt inside a container ignores them without extra configuration.
The private registry that goes through the proxy
A registry on the internal network does not need the proxy and usually cannot be reached through it; the proxy answers with a 403 or fails to connect. Add the registry's hostname to the exclusion list in whichever layer is making the request: no-proxy in daemon.json, NO_PROXY in the drop-in, noProxy in config.json. Docker's documentation notes that the list accepts IP prefixes, domain names, and a leading dot to match subdomains only.
A checklist that ends most cases
- Identify the layer from the error:
proxyconnect, registry URLs, anddocker pullfailures are the daemon; failures insideRUNsteps and running containers are the container layer; on Windows and macOS, Desktop's Proxies screen is the daemon. - Print what the layer believes:
systemctl show --property=Environment dockeranddocker infofor the daemon,docker run --rm alpine env | grep -i proxyfor the container layer. - Fix the file, use
http://schemes, encode credentials, and list internal hosts in the exclusion. systemctl daemon-reloadbeforesystemctl restart dockerwhen the drop-in changed.- For
x509, install the authority in the host's trust store and restart, rather than marking anything insecure.
Where a proxy of your own comes in
The proxies in this guide are the ones an organisation puts between Docker and the internet. A commercial proxy enters the picture for the software running inside containers: scrapers, checkers, and automation that need to reach the web from a residential or fixed address rather than from the host's. That is the container layer, and the same HTTP_PROXY variables carry it; our FlareSolverr guide shows a common case, including the Docker networking trap where 127.0.0.1 inside a container is the container rather than the host.
Three layers, one rule
Three layers: daemon, container, Desktop. proxyconnect errors are the daemon and mean the proxy address is wrong. The TLS-handshake line means an https:// scheme that should be http://. x509 means installing the corporate authority in the trust store. Containers that cannot reach out need their own variables from config.json. And nothing takes effect until daemon-reload and a restart.