Guide

Docker Behind a Proxy: proxyconnect tcp, TLS Handshake Timeout, and x509 Errors Fixed

Docker has three proxy settings: the daemon's, the client's for containers, and Docker Desktop's. Which one each error points at, and the certificate fix.

HProxy Team··7 min read
HProxy.Guide

Skip the dead lists.

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump.

Open the free proxy list

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

Three proxies, three kinds of traffic
  1. Daemon (dockerd)

    pull, push, login: daemon.json or systemd

  2. Containers and builds

    apt, pip, npm inside: ~/.docker/config.json

  3. Docker Desktop

    its own Proxies screen; daemon.json ignored

Source: Docker Engine daemon proxy documentation

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 textLayerWhat happenedFix
Get "https://registry-1.docker.io/v2/": proxyconnect tcp: dial tcp 10.0.0.5:3128: connect: connection refusedDaemonThe daemon's proxy is unreachableFix the address; reload and restart
proxyconnect tcp: tls: first record does not look like a TLS handshakeDaemonhttps:// scheme on a plain HTTP proxyUse http://
Get "https://registry-1.docker.io/v2/": net/http: TLS handshake timeoutDaemonDirect access is blocked, or the proxy stallsSet the daemon proxy, or fix it
x509: certificate signed by unknown authorityDaemon or containerAn inspecting proxy re-signs HTTPSInstall the corporate CA
dial tcp: lookup registry-1.docker.io on ...: no such hostDaemonDNS cannot resolve the registryFix DNS, or the proxy resolves it
Get "https://registry-1.docker.io/v2/": ... i/o timeoutDaemonNo answer at allSame as the handshake timeout
apt-get, pip, or npm fails inside a build or container while docker pull worksContainerThe container has no proxyconfig.json proxies, or -e / --build-arg
A private registry fails with a proxy 403 or connection errorEitherThe internal host is going through the proxyAdd 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

  1. Identify the layer from the error: proxyconnect, registry URLs, and docker pull failures are the daemon; failures inside RUN steps and running containers are the container layer; on Windows and macOS, Desktop's Proxies screen is the daemon.
  2. Print what the layer believes: systemctl show --property=Environment docker and docker info for the daemon, docker run --rm alpine env | grep -i proxy for the container layer.
  3. Fix the file, use http:// schemes, encode credentials, and list internal hosts in the exclusion.
  4. systemctl daemon-reload before systemctl restart docker when the drop-in changed.
  5. 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.

Frequently asked questions

How do I configure Docker to use a proxy?
It depends on which Docker needs it. The daemon, which pulls and pushes images, reads proxies from daemon.json (the http-proxy, https-proxy, and no-proxy keys) or from a systemd drop-in at /etc/systemd/system/docker.service.d/http-proxy.conf with Environment lines, followed by systemctl daemon-reload and a restart. Containers and builds get their proxy from ~/.docker/config.json under proxies.default, which Docker injects as environment variables. Docker Desktop ignores daemon.json proxies and takes the setting from its own Resources, Proxies screen.
What does proxyconnect tcp: dial tcp ... connect: connection refused mean?
The Docker daemon is configured to use a proxy and cannot reach it: nothing is listening at that address and port. The proxy setting in daemon.json or the systemd drop-in is wrong, the proxy is down, or the machine is on a network where the proxy is not reachable. Check what the daemon believes with systemctl show --property=Environment docker and docker info, fix the address, reload systemd, and restart Docker.
Why does Docker say tls: first record does not look like a TLS handshake?
The daemon's https-proxy value starts with https://, so Docker opened a TLS handshake with the proxy, and the proxy answered in plain HTTP. Almost every corporate proxy speaks plain HTTP on its listening port even when it carries HTTPS traffic. Change the scheme to http:// and restart the daemon.
How do I fix x509: certificate signed by unknown authority in Docker?
The proxy inspects HTTPS and re-signs it with the organisation's certificate authority, which the Docker host does not trust. Install the authority's root certificate in the host's trust store (update-ca-certificates on Debian and Ubuntu, update-ca-trust on RHEL and Fedora) and restart Docker, or place it as ca.crt under /etc/docker/certs.d/registry-hostname/ for one registry. On Docker Desktop, install it in the operating system's store and restart Desktop.
Why can my containers not reach the internet even though docker pull works?
Because the daemon's proxy and the container's proxy are separate settings. The daemon proxy covers image pulls; processes inside a container (apt, pip, npm, curl) need HTTP_PROXY and HTTPS_PROXY in the container's environment. Set them under proxies.default in ~/.docker/config.json so Docker injects them at run and build time, or pass them with -e at run time and --build-arg at build time.
How do I keep a private registry off the proxy?
List it in no-proxy (daemon.json), NO_PROXY (systemd drop-in), or noProxy (config.json), alongside localhost and 127.0.0.1. Docker's documentation notes that no-proxy accepts IP prefixes, domain names, and a leading dot for subdomains. Without the exclusion, requests to the internal registry go to the proxy, which usually cannot reach it and answers with a 403 or a connection error.

Get proxies that are alive right now

Our free list re-checks every exit every few minutes and shows a last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup