问题背景
在使用官方 Docker Compose 方式部署 Harbor 时,我们通常会选择在 Harbor 层面关闭 HTTPS,而在前端使用 Nginx 或其他网关来进行 SSL 卸载和反向代理,以实现通过 HTTPS (https://harbor.yourdomain.com) 访问。
然而,在这种经典架构下,当我们在客户端服务器上执行 docker login 时,经常会遇到一个令人困惑的超时错误:
(base) username@ubuntu-server:\~/config$ docker login harbor.yourhost.cn
Username: username
Password:
Error response from daemon: Get "https://harbor.yourhost.cn/v2/": Get "http://harbor.yourhost.cn:8188/service/token?account=username&...": net/http: request canceled (Client.Timeout exceeded while awaiting headers)
这个错误提示非常具有迷惑性:明明我们访问的是 https:// 地址,为什么错误日志里却出现了一个 http:// 地址,并且还带了一个奇怪的端口号(比如这里的 8188)?本文将深入剖析这个问题背后的原因,并提供一劳永逸的解决方案。
错误原因分析
让我们来分解一下 docker login 的整个流程,看看问题出在哪里:
- 客户端发起首次请求: Docker 客户端向 https://harbor.yourhost.cn/v2/ 发起一个未经身份验证的请求,以探测 Harbor Registry 的版本和认证方式。
- Nginx 成功代理: Nginx 接收到这个 HTTPS 请求,解密流量,然后将其转发给后端的 Harbor 服务。在我们的案例中,Harbor 的 HTTP 服务监听在 8188 端口,所以 Nginx 将请求转发到了 http://<harbor-server-ip>:8188。
- Harbor 返回认证地址: Harbor 收到请求后,需要告诉 Docker 客户端:“请到我的认证服务(Token Service)获取一个令牌(Token)再回来。” 问题的关键就在这里!Harbor 如何确定自己的认证服务地址呢?它会查阅自己的配置文件。
- 错误的配置: 在我们的初始配置 harbor.yml 中,是这样设置的:
\# harbor.yml (错误配置)
hostname: harbor.yourhost.cn
http:
port: 8188
Harbor 根据这个配置,认为自己的访问地址就是 http:// + hostname + http.port。因此,它给 Docker 客户端返回的认证地址就是 http://harbor.yourhost.cn:8188/service/token?...。
- 客户端请求认证失败: Docker 客户端非常“听话”,它拿到这个地址后,就立刻尝试去访问。但是,这个 8188 端口很可能只在 Harbor 服务器的内部网络中暴露,或者被防火墙阻挡,客户端根本无法从外部访问到。
- 最终结果: 请求无法建立连接,在等待一段时间后,客户端因超时而放弃,最终抛出了我们看到的 Client.Timeout exceeded 错误。
简单来说,Harbor 并不知道它正“生活”在一个 Nginx 的 HTTPS 反向代理之后。它错误地将自己的内部 HTTP 地址告诉了外部客户端,导致了通信失败。
解决方案:让 Harbor 感知到公开 URL
要解决这个问题,我们必须明确地告诉 Harbor,它的公开访问地址是什么。这需要通过修改 harbor.yml 文件中的一个关键参数:external_url。
1. 修改 harbor.yml
进入你的 Harbor 安装目录,编辑 harbor.yml 文件。
- 注释掉或删除 hostname 参数。
- 添加 external_url 参数,并将其值设置为你完整的、公开的 HTTPS 地址。
修改后的配置如下:
\# harbor.yml (正确配置)
\# 注释掉或者直接删除 hostname
\# hostname: harbor.yourhost.cn
\# http port 保持不变,这是 Harbor 内部监听的端口,供 Nginx 访问
http:
port: 8188
\# https:
\# ... (保持注释状态,因为 SSL 由 Nginx 处理)
\# 这是关键!设置完整的外部访问URL,必须以 https:// 开头
external\_url: https://harbor.yourhost.cn
这个 external\_url 参数会作为 Harbor 生成所有对外链接(包括 API、UI 和认证服务)的根地址。
2. 重新配置并启动 Harbor
保存 harbor.yml 文件后,你需要让这个新配置生效。在 Harbor 的安装目录下,执行以下命令:
# 1. 停止并移除旧的容器实例
# -v 选项会删除挂载的数据卷,如果需要保留镜像数据,请先备份或省略 -v
sudo docker-compose down -v
# 2. 根据修改后的 harbor.yml 重新生成所有服务的配置文件
# 这一步是必须的,它会将 external_url 的值写入到各个组件的配置中
sudo ./prepare
# 3. 重新启动所有 Harbor 服务
sudo docker-compose up -d
3. 验证结果
等待所有容器启动成功后(可以通过 docker-compose ps 查看状态),回到你的客户端服务器,再次尝试登录:
docker login harbor.yourhost.cn
Username: username
Password:
Login Succeeded
现在,登录成功了!
附:推荐的 Nginx 配置
虽然问题主要出在 Harbor 的配置上,但一个健壮的 Nginx 反向代理配置同样重要。请确保你的配置包含了必要的请求头,特别是 X-Forwarded-Proto,它可以再次向后端应用表明原始请求的协议。
server {
listen 443 ssl http2;
server\_name harbor.yourhost.cn;
\# SSL 证书配置
ssl\_certificate /path/to/your/fullchain.pem;
ssl\_certificate\_key /path/to/your/privkey.pem;
ssl\_protocols TLSv1.2 TLSv1.3;
\# 安全与性能优化
add\_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
client\_max\_body\_size 0; \# 允许上传大镜像
location / {
\# 代理到Harbor的内部HTTP端口
proxy\_pass http://127.0.0.1:8188;
\# 关键的代理头信息
proxy\_set\_header Host $host;
proxy\_set\_header X-Real-IP $remote\_addr;
proxy\_set\_header X-Forwarded-For $proxy\_add\_x\_forwarded\_for;
proxy\_set\_header X-Forwarded-Proto $scheme; \# $scheme 的值通常是 http 或 https
proxy\_set\_header X-Forwarded-Port $server\_port;
\# WebSocket 支持 (用于Web UI)
proxy\_http\_version 1.1;
proxy\_set\_header Upgrade $http\_upgrade;
proxy\_set\_header Connection "upgrade";
\# 超时设置
proxy\_connect\_timeout 600s;
proxy\_send\_timeout 600s;
proxy\_read\_timeout 600s;
}
}
\# (可选) 将所有HTTP请求强制重定向到HTTPS
server {
listen 80;
server\_name harbor.yourhost.cn;
return 301 https://$server\_name$request\_uri;
}
总结
在部署 Harbor 并使用 Nginx 进行 HTTPS 反向代理时,docker login 超时是一个典型但容易解决的问题。其根源在于 Harbor 自身配置未能感知到外部访问的协议和地址。通过在 harbor.yml 中正确设置 external_url 为公开的 HTTPS 地址,即可彻底解决此问题,确保 Harbor 生成的认证地址正确无误。