平台和 DevOps

net/http: TLS handshake timeout:Docker 拉镜像超时的修法

作者: · 最后核实

报错信息net/http: TLS handshake timeout

net/http: TLS handshake timeout 的意思是:Docker 已经通过 TCP 连上了镜像仓库,但 TLS 握手在 10 秒内没有完成。 这是网络问题,不是证书问题。常见原因是到仓库的链路慢或丢包、Docker 守护进程没有使用你终端里的代理、MTU 问题导致大包被丢弃,或者配置的镜像加速地址已经不响应了。

这条报错是什么意思

这条消息来自 Go 的 net/http 包,Docker、containerd、kubectl、Helm 都基于它。Go 的 http.Transport 在握手时间超过 TLSHandshakeTimeout 时放弃;Docker 守护进程访问仓库的 Transport 把它设为 10 秒(TCP 连接超时为 30 秒)。通常你会看到:

Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: TLS handshake timeout

docker pull 是让守护进程去下载镜像。守护进程从自己的环境变量读取代理(代码里是 http.ProxyFromEnvironment),你在终端里 export 的变量对它不起作用。

常见原因(按出现频率排序)

最常见的原因是到仓库的链路慢、拥塞或丢包,其次是守护进程需要代理却没配置。

1. 到仓库的链路慢、拥塞或丢包

跨境链路、拥挤的 Wi-Fi、丢包严重的网络,都会让握手需要的几个来回超过 10 秒。在国内直接访问 Docker Hub 时这种情况尤其常见。

  • 确认: 在同一台机器上测量握手耗时:
curl -sS -o /dev/null -w 'connect=%{time_connect}s tls=%{time_appconnect}s http=%{http_code}\n' https://registry-1.docker.io/v2/

正常结果是 HTTP 401(需要认证),tls 远小于 1 秒。如果要好几秒甚至卡住,说明在 Docker 之外也能复现。

  • 修复: 使用就近的镜像加速或拉取缓存,或者把构建放到链路更好的网络里。

2. 守护进程需要代理却没配置

公司网络和部分云网络只允许通过代理访问外网 HTTPS。终端里设置了 HTTPS_PROXY,守护进程却没有。

  • 确认:
docker info | grep -iE "http proxy|https proxy|no proxy"
sudo systemctl show --property=Environment docker

3. MTU 黑洞

VPN、PPPoE 拨号和部分云网络的路径 MTU 小于 1500,同时“需要分片”的 ICMP 消息又被拦截。小的 ClientHello 能过去,服务器发来的较大证书数据包过不来,握手就卡住了。

  • 确认: 用 ip link show 查看网卡 MTU,再执行 ping -M do -s 1472 registry-1.docker.io(Linux)。出现 “Message too long” 或没有响应、而更小的包能通,就是 MTU 问题。
  • 修复: 把网卡 MTU 设成链路实际支持的值,或者在 VPN、路由器上开启 MSS 钳制。如果是容器内构建受影响,在 daemon.json 里设置默认网桥的 "mtu"。

4. 配置的镜像加速地址变慢或失效

registry-mirrors 里填的地址不再响应,就会出现针对这个加速地址的超时。

  • 确认: docker info | grep -A3 "Registry Mirrors",再用上面的 curl 命令逐个测试加速地址。
  • 修复: 删掉失效的地址,重启守护进程。

5. 防火墙或检测设备拖住了 TLS

有些设备会把 TLS 会话扣下来检查,或者对特定目标限速。如果 curl 测试在别的网络很快、只在这里慢,请网络管理员排查。

分平台修复命令

各平台的核心修法是给守护进程本身配代理,并重启 Docker 让配置生效。

Linux,通过 daemon.json 配置守护进程代理(Docker 推荐的方式):

{
  "proxies": {
    "http-proxy": "http://proxy.example.com:3128",
    "https-proxy": "http://proxy.example.com:3128",
    "no-proxy": "localhost,127.0.0.1,.example.com"
  }
}
sudo systemctl restart docker

Linux,systemd drop-in(/etc/systemd/system/docker.service.d/http-proxy.conf):

[Service]
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1"
sudo systemctl daemon-reload && sudo systemctl restart docker

Docker Hub 镜像加速(/etc/docker/daemon.json):

{ "registry-mirrors": ["https://mirror.example.com"] }

国内服务器可以填云厂商控制台里给出的镜像加速地址(如果该厂商提供),或者公司内部自建的拉取缓存。加速地址只对 Docker Hub 镜像生效,改完要重启守护进程,并用 docker info 确认已加载。公共加速服务随时可能调整或停止,生产环境更稳妥的做法是把常用镜像同步到自己的私有仓库(比如云厂商的容器镜像服务)。

Docker Desktop(Windows 和 macOS): 设置 → Resources → Proxies(可以跟随系统代理);设置 → Docker Engine 里编辑 daemon.json,填写镜像加速地址。

使用 containerd 的 Kubernetes 节点: kubelet 事件里也会出现同样的 Go 报错,代理要配置在 containerd 服务的环境里,而不是 Docker 的。

不推荐:关闭证书校验

把仓库加进 insecure-registries,或者为了“提速”在加速器、代理上关闭 TLS 校验,都修不好超时,反而去掉了镜像完整性的保护。通过未经校验的连接拉取镜像,链路上的任何人都可以把它换成恶意镜像。

更安全的替代办法:配好守护进程代理;使用可信的 HTTPS 镜像加速;修正 MTU;如果代理会重新签名 TLS,就把它的根证书放进 /etc/docker/certs.d/<仓库地址>/ca.crt 或系统信任库。

怎么确认已经修好

守护进程信息里能看到你配置的代理和加速地址,docker pull 不再超时,就说明修好了:

docker info | grep -iE "proxy|mirrors" -A1
time docker pull hello-world

docker build 里的 RUN apt-get、RUN pip install 是在构建容器里执行的,走的是构建容器的网络和代理设置,和守护进程拉镜像不是一回事,需要通过构建参数单独传入代理。如果只是偶发超时,先重试一两次再下结论;如果每次都超时,再按上面的顺序逐项排查。改完守护进程配置要重启 Docker,否则新设置不会生效。

再跑一次上面的 curl 计时命令,TLS 耗时在一秒以内说明链路正常。握手的过程以及大数据包出现在哪一步,见 TLS 握手过程。

常见问题

为什么 curl 能访问,docker pull 却超时?

终端和 Docker 守护进程是两个不同的进程。curl 用的是终端里的 HTTPS_PROXY;守护进程由 systemd 或 Docker Desktop 启动,只认 daemon.json、systemd drop-in 或 Docker Desktop 设置里的代理。用 docker info | grep -i proxy 查看守护进程实际使用的代理。

能把 10 秒超时调大吗?

不能。守护进程没有公开的配置项可以改它,这个值写在仓库访问的代码里。应该去修链路:正常情况下 TLS 握手一秒之内就能完成。如果链路确实慢,就近使用镜像加速或拉取缓存(pull-through cache)才是实际可行的办法。

把仓库加到 insecure-registries 有用吗?

没用。insecure-registries 允许对某个仓库使用明文 HTTP 或不受信任的证书,并不能让慢握手变快;对公开仓库来说,它只会去掉防止镜像被篡改的保护。只应该用于你自己控制的私有测试仓库。

镜像加速器对 ghcr.io、quay.io 或私有仓库有效吗?

无效。Docker 文档写明 registry-mirrors 只能镜像 Docker Hub。其他仓库的镜像仍然直连,它们的握手问题要靠代理或网络层面的方法解决;国内一些云厂商为其他仓库提供单独的同步或代理服务,要按各自文档配置。

资料来源

  1. Docker docs: Daemon proxy configuration
  2. Docker docs: Registry mirror (registry-mirrors)
  3. Docker docs: dockerd reference (daemon.json keys, --mtu)
  4. Go docs: net/http Transport (TLSHandshakeTimeout)
  5. moby source: daemon/pkg/registry/registry.go (transport timeouts)