记一次 Docker 镜像拉不下来的坑:镜像代理 + HTTP 代理的组合拳
背景
我的博客用 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 → 走代理
而且镜像层有缓存,第一次拉完整镜像后,后续部署只拉增量层,代理流量几乎可以忽略。
总结
- Docker 镜像代理(
registry-mirrors)只代理公开白名单镜像,私有镜像不在其中 docker pull前设HTTP_PROXY没用,代理要配在 daemon 层面- daemon 代理 + 镜像代理组合使用:公共镜像走代理源(省钱),私有镜像走 HTTP 代理(能拉到)
NO_PROXY配好内网和镜像代理地址,避免不必要的代理流量
说实话这个问题折腾了我好几次,每次 GitHub Actions 部署失败都很烦。这次算是彻底解决了。