记一次 Docker 镜像拉不下来的坑:镜像代理 + HTTP 代理的组合拳

2026年06月30日3 次阅读0 人喜欢
Docker踩坑记录运维代理CI/CD

背景

我的博客用 GitHub Actions 构建 Docker 镜像,推到 Docker Hub,然后 SSH 到服务器拉取部署。这套流程跑了大半年一直没问题,直到最近 GitHub Actions 日志里服务器部署这一步开始报错:

复制代码
Error response from daemon: Get "https://registry-1.docker.io/v2/": 
net/http: request canceled while waiting for connection 
(Client.Timeout exceeded while awaiting headers)

拉了 3 次,每次超时,全部失败。

排查过程

镜像代理不是万能的

服务器上的 /etc/docker/daemon.json 早就配了镜像代理:

json 复制代码
{
  "registry-mirrors": [
    "https://docker.m.daocloud.io",
    "https://docker-proxy.nnnnzs.cn"
  ]
}

那为什么还拉不下来?因为镜像代理只代理 Docker Hub 上的公开白名单镜像。我自己的镜像 nnnnzs/react-nnnnzs-cn 不在白名单里,代理直接拒绝:

复制代码
pull access denied for docker.m.daocloud.io/nnnnzs/react-nnnnzs-cn, 
repository does not exist or may require 'docker login': 
denied: ? 这镜像不在白名单

docker-proxy.nnnnzs.cn 虽然不挑镜像,但它本质上是转发到 Docker Hub,同样被墙,超时。

为什么 Docker Hub 官方地址也不通?

服务器在腾讯云,国内访问 registry-1.docker.io 本来就不稳定,最近更是直接超时。所以整个链路断了:

  • 镜像代理 → 拒绝私有镜像 ❌
  • Docker Hub 官方 → 被墙超时 ❌
  • 自己的代理 → daemon 不认 ❌

为什么 docker pull 时设环境变量没用?

我一开始想在部署脚本里写 HTTP_PROXY=xxx docker pull,结果发现完全没用。因为 HTTP_PROXY 是给客户端进程的,但实际拉镜像的是 Docker daemon 进程,它看不到你传给 docker 命令的环境变量。

解决方案:daemon 代理 + 镜像代理组合

最终方案是给 Docker daemon 配上 HTTP 代理,让它通过 ZeroTier 网络走家里的 Clash 代理拉镜像。

创建 systemd override 文件:

bash 复制代码
sudo mkdir -p /etc/systemd/system/docker.service.d

cat > /etc/systemd/system/docker.service.d/proxy.conf << EOF
[Service]
Environment="HTTP_PROXY=http://192.168.80.80:7890"
Environment="HTTPS_PROXY=http://192.168.80.80:7890"
Environment="NO_PROXY=localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,docker.m.daocloud.io"
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

注意 NO_PROXY 里加了 docker.m.daocloud.io,这样访问镜像代理时不走代理,避免绕路。

关键点:流量其实很小

你可能会担心:给 daemon 配了全局代理,以后所有镜像都走代理,流量费扛不住。

其实不会。Docker 拉镜像的逻辑是:先查镜像代理,没有再走主仓库

  • node:22-alpine 这种公共基础镜像 → daocloud 有 → 直接从 daocloud 拉,不走代理
  • nnnnzs/react-nnnnzs-cn 这种私有镜像 → daocloud 没有 → 回退到 Docker Hub → 走代理

而且镜像层有缓存,第一次拉完整镜像后,后续部署只拉增量层,代理流量几乎可以忽略。

总结

  1. Docker 镜像代理(registry-mirrors)只代理公开白名单镜像,私有镜像不在其中
  2. docker pull 前设 HTTP_PROXY 没用,代理要配在 daemon 层面
  3. daemon 代理 + 镜像代理组合使用:公共镜像走代理源(省钱),私有镜像走 HTTP 代理(能拉到)
  4. NO_PROXY 配好内网和镜像代理地址,避免不必要的代理流量

说实话这个问题折腾了我好几次,每次 GitHub Actions 部署失败都很烦。这次算是彻底解决了。

加载评论中...