The 'net/http: TLS handshake timeout' error occurs when Docker cannot complete a TLS handshake with a registry within the allowed time. It is usually caused by MTU mismatches, proxy misconfiguration, firewall/VPN packet inspection, or network latency rather than the registry being down.
The "net/http: TLS handshake timeout" error occurs during the initial security handshake phase when Docker tries to establish an encrypted connection to a container registry like Docker Hub (registry-1.docker.io). The TLS handshake is the process where your Docker client and the registry server exchange cryptographic keys and certificates to establish a secure, encrypted connection. When this handshake takes longer than the timeout period (typically 10-15 seconds), Docker aborts the connection and displays this error. Unlike a connection refused error where the server actively rejects the connection, a TLS handshake timeout means the TCP connection was established but the encrypted negotiation could not finish in time. This points to packets being delayed, dropped, or partially blocked somewhere in the network path between Docker and the registry. It is common in corporate networks with deep packet inspection, on VPN connections, or when there are MTU (Maximum Transmission Unit) size mismatches that silently drop large handshake packets.
First, determine if the issue is Docker-specific or a general network problem:
# Test TLS connection to Docker Hub
curl -v https://registry-1.docker.io/v2/
# Test DNS resolution
nslookup registry-1.docker.io
# Check if you can reach the auth server
curl -v https://auth.docker.io/If curl succeeds but Docker fails, the issue is likely with Docker's network or daemon configuration. If both fail, you have a network-level problem (MTU, proxy, firewall, or VPN).
MTU mismatches are the single most common cause of TLS handshake timeouts, especially on VPN, cloud, or virtualized networks. When the daemon sends handshake packets larger than the path can carry, they are silently dropped and the handshake stalls. Lowering the MTU is the fix that resolves this for most people, so try it early:
# Edit Docker daemon configuration
sudo nano /etc/docker/daemon.jsonAdd or update the MTU setting:
{
"mtu": 1400
}If that does not work, try an even lower value like 1300:
{
"mtu": 1300
}Restart Docker to apply:
sudo systemctl restart dockerNote: The default MTU is 1500, but VPNs and some cloud networks use smaller values due to encapsulation overhead. You can confirm your real path MTU with ip link show on the host interface, or by testing with ping -M do -s 1400 registry-1.docker.io.
If you are behind a corporate proxy, configure Docker with the correct proxy settings:
# Create the systemd drop-in directory
sudo mkdir -p /etc/systemd/system/docker.service.d
# Create the proxy configuration file
sudo nano /etc/systemd/system/docker.service.d/proxy.confAdd your proxy settings. Important: Use http:// (not https://) for the HTTPS_PROXY value if your proxy does not terminate TLS itself:
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:8080"
Environment="HTTPS_PROXY=http://proxy.example.com:8080"
Environment="NO_PROXY=localhost,127.0.0.1"Reload and restart Docker:
sudo systemctl daemon-reload
sudo systemctl restart dockerVerify the settings:
sudo systemctl show --property=Environment dockerTLS certificate validation depends on an accurate system clock. If the host clock is significantly wrong, certificate checks can stall or fail during the handshake and surface as a timeout. Verify and correct the time:
# Check current time and sync status
timedatectl status
# Enable NTP sync if it is off
sudo timedatectl set-ntp true
# Force an immediate resync (systemd-timesyncd)
sudo systemctl restart systemd-timesyncdThis is especially common on freshly provisioned VMs, suspended laptops, and containers whose clock has drifted.
DNS issues can delay connection setup and contribute to timeouts. Configure Docker to use reliable DNS servers:
# Edit Docker daemon configuration
sudo nano /etc/docker/daemon.jsonAdd DNS settings:
{
"dns": ["8.8.8.8", "8.8.4.4"]
}Restart Docker:
sudo systemctl restart dockerFor Docker Desktop (Windows/Mac): Go to Settings > Docker Engine and add the DNS configuration to the JSON editor.
VPNs and firewalls with deep packet inspection or TLS interception can interfere with handshakes:
1. Temporarily disconnect your VPN and retry the Docker command.
2. Temporarily disable your firewall only as a diagnostic, then re-enable it:
# On Ubuntu/Debian
sudo ufw disable
docker pull nginx:latest
sudo ufw enable
# On CentOS/RHEL
sudo systemctl stop firewalld
docker pull nginx:latest
sudo systemctl start firewalldIf Docker works without the VPN/firewall, do not leave them off. Instead configure proper exceptions:
- Allow outbound TLS (port 443) to registry-1.docker.io and auth.docker.io
- Add Docker Hub domains to your VPN split-tunnel configuration
- Disable TLS inspection specifically for registry traffic, or install the inspection proxy's CA certificate so Docker trusts the re-signed certs
Sometimes the host prefers an unroutable or slow IPv6 path to the registry while IPv4 works fine, which stalls the handshake. Prefer targeted fixes over disabling IPv6 system-wide.
First, confirm the IPv6 path is the problem:
# Compare IPv4 vs IPv6 reachability
curl -4 -v https://registry-1.docker.io/v2/
curl -6 -v https://registry-1.docker.io/v2/If only IPv6 hangs, bias address selection toward IPv4 via gai.conf instead of turning IPv6 off:
# Add to /etc/gai.conf to prefer IPv4 for outbound connections
echo 'precedence ::ffff:0:0/96 100' | sudo tee -a /etc/gai.confThis affects host-level getaddrinfo ordering without removing IPv6.
Last resort and strong caveat: Disabling IPv6 host-wide (via net.ipv6.conf.all.disable_ipv6=1 in sysctl) is broad and can break other services that rely on IPv6. Only do this if you fully control the host and have ruled out the targeted fixes above.
Sometimes Docker holds a stale connection or auth token. A restart plus a fresh login clears transient state:
# Restart Docker daemon
sudo systemctl restart docker
# Clear and refresh registry credentials
docker logout
docker login
# Retry the pull
docker pull nginx:latestIf the error is intermittent, use a retry loop:
for i in {1..5}; do
docker pull nginx:latest && break
echo "Attempt $i failed. Retrying in 10 seconds..."
sleep 10
doneFor CI/CD pipelines, add retry logic with backoff to absorb transient handshake failures.
Occasionally a specific Docker release introduces a regression that affects registry connectivity. Rather than assuming a particular version is broken, check what you are running and compare it against the official release notes and issue tracker for your version:
docker version- Review the [Docker Engine release notes](https://docs.docker.com/engine/release-notes/) for fixes related to networking or TLS for your version.
- Search the moby/moby GitHub issues for your exact version and symptom.
- If a documented regression matches, upgrade to a release where it is fixed (preferred), or pin to the last known-good version for your distro, e.g.:
# Example: install a specific version on Ubuntu/Debian
apt-cache madison docker-ce
sudo apt-get install docker-ce=<version-string>Upgrading forward is generally safer than downgrading.
If the registry is consistently slow or unreliable from your location, a mirror can help. Pick a mirror you can rely on:
# Edit daemon.json
sudo nano /etc/docker/daemon.json{
"registry-mirrors": ["https://your-mirror.example.com"]
}Restart Docker:
sudo systemctl restart dockerChoosing a mirror:
- Prefer a mirror your cloud provider offers in your region, or an internal pull-through cache (such as a self-hosted registry in mirror mode).
- Note that Google's public mirror.gcr.io has been deprecated/limited and is no longer a dependable general-purpose Docker Hub mirror, so do not rely on it as your primary fix.
- For specific images, alternative registries like ghcr.io or quay.io may be faster or more reliable.
### Understanding the TLS Handshake
The TLS handshake involves multiple round trips between client and server:
1. Client Hello (client sends supported cipher suites)
2. Server Hello (server responds with chosen cipher)
3. Certificate exchange
4. Key exchange
5. Finished (encrypted channel established)
Any delay or packet loss during these steps can cause a timeout. The error does not mean the server is unreachable - it means the cryptographic negotiation could not complete in time. Because the handshake exchanges relatively large certificate packets, it is unusually sensitive to MTU/fragmentation problems, which is why lowering the MTU resolves so many cases.
### Docker Desktop Specific Issues
Windows:
- Check Windows Firewall rules for Docker
- Ensure the Hyper-V/WSL network adapter has correct DNS settings
- Try resetting the WSL 2 backend: wsl --shutdown then restart Docker Desktop
macOS:
- Reset Docker Desktop via Preferences > Troubleshoot > Reset to factory defaults
- Check whether the macOS firewall is blocking Docker
### Debugging with Verbose Output
Enable debug logging to see more details:
# Edit daemon.json
sudo nano /etc/docker/daemon.json{
"debug": true
}View logs:
sudo journalctl -u docker -f### Proxy Protocol Considerations
A common mistake is using https:// for the HTTPS_PROXY value. Most corporate proxies expect a plain HTTP connection from the client even when proxying HTTPS traffic:
# Wrong (often causes TLS handshake issues):
Environment="HTTPS_PROXY=https://proxy.example.com:8080"
# Correct:
Environment="HTTPS_PROXY=http://proxy.example.com:8080"The proxy handles the TLS connection to the destination server, so your connection to the proxy itself does not need to be HTTPS.
### CI/CD Pipeline Considerations
In CI/CD environments:
- Add retry logic with exponential backoff
- Cache base images in your own registry or pull-through cache
- Use authenticated pulls to avoid rate limiting
- Consider a registry mirror provided by your CI platform
### Verifying Typos
Double-check your image name - a typo can also produce confusing errors because Docker tries to connect to a non-existent repository.
Container exited with code 128: invalid exit argument
How to fix 'Container exited with code 128' in Docker
Error response from daemon: Get https://registry-1.docker.io/v2/: Proxy Authentication Required
How to fix 'Proxy Authentication Required' in Docker
error exporting cache: failed to export cache
How to fix 'error exporting cache: failed to export cache' in Docker
Docker container exited with code 255
How to fix 'Container exited with code 255' in Docker
Read-only file system
How to fix 'Read-only file system' in Docker