前一阵子帮同事排查一个推送镜像失败的场景,docker login都成功了,Harbor页面也登录得上,偏偏docker push就报unauthorized。折腾一圈,根因是那台机器上docker客户端缓存了旧的凭证,重新执行login时又因为终端开了代理环境变量,请求打到了别的地址。这种问题在推送镜像到Harbor时特别典型。很多人都觉得docker push嘛,就是一条命令的事,但真正在CI/CD里跑起来,或者在跨网络环境手工推送的时候,你会发现它背后牵扯的是客户端配置、仓库权限、镜像命名、网络策略、证书信任、存储空间这一整套链路。这篇文章就把整个push链路上最容易出问题的点逐一拆开,按实际排查的顺序来讲,尽量把那些官方文档不会明说、但你必须知道的坑都点到位。1. 动手push前,先把这三件事看清楚1.1 镜像tag里的仓库地址,到底该怎么写有不少人第一次拿到Harbor的访问地址,直接拿web页面的URL去拼镜像tag,结果要么端口多写了,要么路径少写了一层。Harbor的镜像地址遵循的标准格式是:<Harbor主机地址>:<端口>/<项目名>/<镜像名>:<tag>注意几个容易写错的地方:端口不是必填项。如果Harbor通过80端口对外提供HTTP服务,那么harbor.example.com/library/demo:v1是合法的;如果端口是8443、30002这类非默认端口,则必须显式写进tag里,例如harbor.example.com:8443/library/demo:v1。项目名(project)这一层必须存在。Harbor和Docker Hub不同,Harbor里镜像必须归属到某个项目下。管理员如果开启了自动创建项目,你可能还能侥幸推送不存在的项目名,但正规生产环境一般都会关掉这个开关。项目不存在时,服务端直接拒绝,根本不会给你创建的机会。镜像名这一层只能使用小写字母、数字、下划线、点号和横线。大写字母或者斜杠以外的特殊字符都会触发校验错误,有的客户端会直接提示invalid reference format,有的会等到服务端才返回报错。1.2 项目是否存在,以及项目下的权限Harbor的项目类型分为公开和私有。公开项目意味着只要Harbor服务可达,任何人都可以拉取镜像,但推送仍然需要认证。私有项目则拉取和推送都需要登录。我建议大家在push前先确认一件事:目标项目的名字到底是不是你以为的那个。生产环境常见场景是:开发环境里项目叫demo-server,测试环境项目叫demo-server-test,因为环境不同导致tag前缀打错,最后镜像推进了错误的项目,回滚时才发现找不到正确的镜像版本。这种事真的不少见。如果项目不存在、或者你登录的账号没有该项目的push权限,服务端一般会返回403 forbidden。注意,有些Harbor版本会返回401 unauthorized,这是服务端为了防止探测项目信息,故意把「没权限」和「没登录」统一处理了。所以遇到401时,除了检查用户名密码,还要回头看下权限分配。1.3 网络连通性和出网策略推送前先确认客户端到Harbor的443/80端口是否可达。很多时候网络问题不是Harbor这边挂了,而是中间有防火墙、安全组或者代理干扰。这里有一个很典型的坑:公司内网机器如果设置了HTTP代理环境变量(http_proxy、https_proxy、no_proxy),docker客户端访问内网Harbor时默认也会走代理。如果代理服务器不能解析内网域名,或者代理策略拦截了内网地址,就会超时或报连接错误。正确的做法是在no_proxy里加上Harbor的域名和IP段,确保内网流量直连。另外,Harbor如果做了域名解析,而你本地/etc/hosts里写了测试解析,和实际线上IP不一致,也会出现奇怪的连接被拒。建议先用curl -v <harbor地址>/v2/验证连通性和协议,再执行docker命令。这样可以把网络层和docker客户端的问题分开排查。2. 登录认证体系,比表面上要复杂2.1 三种常见账号形态和适用场景Harbor的账号体系主要有三类:本地用户、管理员账号、机器人账号。日常开发中很多人喜欢直接用管理员账号或者某个共用账号登录,图省事,但这在稍微规范一点的团队里都是禁忌。账号类型适用场景权限范围本地用户开发人员手工推送、拉取按被加入的项目授权管理员账号平台初始化、系统配置、维护全平台权限,尽量少用机器人账号CI/CD流水线、自动化脚本仅限指定项目,可限定有效期尤其是CI/CD流水线,千万不要在构建脚本里写死一个本地账号的密码。一方面密码定期变更会导致流水线突然失败,另一方面密码会出现在日志、变量文件等地方,泄露风险大。Harbor的机器人账号是为此设计的:创建时为某个项目生成一段token,用户名形如robot$<机器人名称>,推送时直接当成账号和密码用。这里有个小细节容易坑到人:机器人账号的用户名里带了$符号,在bash脚本里如果不对$做转义,$robot会被当成Shell变量展开,实际发送的账号就变了。在命令行使用时一定要加引号:docker login harbor.example.com -u 'robot$deploy' -p 'xxxxxx'2.2 凭证缓存与登录状态失效docker login成功之后,凭证会写入客户端配置目录。Linux下默认是~/.docker/config.json,其中auths字段保存Base64编码的用户名密码。问题在于:如果机器上有新旧两套配置,或者你设置了DOCKER_CONFIG环境变量指向别的目录,可能登录的目标和实际读取的凭证不在一个地方。一旦密码或机器人token变更过,旧的凭证还留在config.json里,docker login虽然能覆盖,但你如果没有重新执行,下一次push读到旧凭证就会401。在共享的构建机上,还可能存在多个用户使用同一套docker配置的情况,互相覆盖凭证,导致「刚才明明能登录,怎么过会儿就不行了」。我的建议是:自动化脚本里每次push前强制执行一次docker login,并检查退出码。不要依赖「之前已经登录过了」,因为凭证状态是不可信的。登录时可以使用--password-stdin避免密码出现在进程列表里:echo "$HARBOR_ROBOT_TOKEN" | docker login harbor.example.com -u 'robot$deploy' --password-stdin2.3 时间同步对认证的影响这个坑隐蔽性极高。Harbor在鉴权时会校验时间窗口,如果客户端主机、Harbor服务器或者后端认证服务的系统时间不一致,即使账号密码完全正确,也会出现401或token解析失败。很多虚拟机环境的时间同步策略不严格,开机几个月后时间偏差达到几分钟很常见。遇到诡异的认证失败问题,先看一眼date,再做时间同步,往往几十秒就解决了。3. 镜像命名、tag与仓库规则3.1 为什么push前常常要先docker tag很多人搞不清楚什么时候需要docker tag,总觉得docker push就能直接换仓库名。实际上,docker push的语义是「把本地已有的某个tag所指向的镜像,推到该tag指定的仓库地址」,它不会帮你自动改名。如果本地镜像名是demo-server:1.0.0,而你希望推送到Harbor里的library/demo-server:1.0.0,就必须先执行:docker tag demo-server:1.0.0 harbor.example.com:8443/library/demo-server:1.0.0这个操作并不会重新打包镜像,它只是给同一个镜像增加了一个引用名,类似文件系统里的硬链接。本地磁盘占用不会翻倍,可以放心操作。在CI场景里,我经常看到流水线里构建完镜像后输出的名称,和实际docker push使用的名称不一致,导致镜像推到了别的仓库或者漏推了。一个有效的做法是在流水线一开始就统一用完整仓库名作为镜像名来构建,比如docker build -t harbor.example.com:8443/project/app:${BUILD_NUMBER} .,后面直接push,省掉中间的tag步骤,也少一层出错机会。3.2 tag不可变与重复推送生产Harbor往往开启了「Tag不可变」策略,防止同名tag被恶意或误操作覆盖。这时候如果你尝试推送一个已经存在且不可变的tag,服务端会返回denied或405 method not allowed。需要注意,这个策略是项目级别的,不同项目可以配置不同规则。CI里如果每次都用latest作为tag,并且Harbor开启了不可变,第二次构建就会直接失败。更合理的做法是:固定版本:app-${BUILD_NUMBER}-${GIT_COMMIT_SHORT},每次构建生成唯一tag。稳定性版本:app-${MAJOR}.${MINOR}.${PATCH},仅在发布时更新。业务标签:latest、stable,通过运维平台或自动化任务在发版后更新。这种tag策略能避免「同名tag指向不同镜像」带来的环境一致性灾难,也便于按版本号回滚。3.3 跨仓库复制可能带来的digest混乱Harbor自带复制规则,可以把某个项目下的镜像自动同步到另一个Harbor实例或项目下。被复制过来的镜像虽然能拉取,但它和源镜像的存储结构不一定完全一致,尤其是在跨Harbor实例、后端存储类型不同的情况下。如果你在push时遇到manifest already exists这类提示,先别急着以为镜像推上去了。建议push完成后立即用docker manifest inspect或Harbor API核对一下目标tag的digest,确保实际内容是自己构建的那一个。4. 客户端侧配置:证书和daemon.json4.1 HTTPS、HTTP和insecure-registriesHarbor生产环境基本都是HTTPS,但个人测试环境、内网环境里以HTTP方式跑的场景也不少。Docker客户端默认在后端使用HTTPS协议访问仓库,如果Harbor只支持HTTP,而你直接docker push,会出现类似http: server gave HTTP response to HTTPS client的报错。解决办法是在Docker守护进程配置里,把该Harbor地址加入insecure-registries。Linux下编辑/etc/docker/daemon.json:{ "insecure-registries": [ "harbor.example.com", "harbor.example.com:80" ] }改完必须重启docker守护进程:sudo systemctl restart docker需要注意的是,insecure-registries的含义是「允许对该地址使用HTTP或跳过证书校验」,并不是「只对该地址使用HTTP」。如果你配置的地址是harbor.example.com,同时Harbor也在80端口提供HTTP,push时docker会先尝试HTTPS,不行再退回HTTP。为了减少不必要的重试和日志误导,最好把带端口的地址和不带端口的地址都写清楚,并保证配置内容一致。4.2 私有CA证书的正确配置位置很多企业内部Harbor使用的是自签CA或私有CA签发的证书。在这种情况下,即使你在insecure-registries里加了地址,docker login也可能成功,但push时依旧报证书校验错误。原因在于docker push时它会更严格地校验TLS证书链,跳过校验的配置未必对每个操作都生效。更规范的做法是把Harbor使用的CA根证书,放到docker客户端信任目录下:/etc/docker/certs.d/<Harbor地址>:<端口>/ca.crt例如Harbor地址是harbor.example.com:8443,那么目录和文件名就是:/etc/docker/certs.d/harbor.example.com:8443/ca.crt注意,目录名中的端口只有在端口非443时才需要写,443时可以省略。配置完成后同样重启docker。这样配置的好处是,不用为了跳过校验而牺牲TLS安全性,同时也能避免某些镜像仓库对「跳过证书校验」的额外限制。4.3 Windows和macOS Docker Desktop的特殊性如果你在Windows或macOS上用Docker Desktop,daemon.json的位置和Linux不一样。Docker Desktop提供了图形界面叫Docker Engine,可以直接编辑配置JSON,它实际管理的还是一个daemon.json,但路径在虚拟机的内部。界面修改完点Apply就会生效。另外,Windows下走PowerShell推送时,记得检查代理设置和网络配置文件,尤其是公司域控环境下,系统代理可能自动注入,导致docker命令访问的地址不是预期目标。4.4 重启docker守护进程带来的副作用修改了daemon.json之后要重启docker,这个操作会把运行中的容器全部停掉。如果是生产机器上正在跑业务容器,重启前一定要提前规划迁移或发布窗口。很多同学为了省事,push前临时在daemon.json里加insecure-registries,然后重启,把正在跑的服务搞挂了,这就属于典型的低级事故。5. 推送过程本身的性能、分块与稳定性问题5.1 大镜像推送为什么那么慢单个镜像大的时候,docker push会把镜像拆成多个layer分块上传。Docker客户端默认会开启并行上传,但并行度受配置影响,同时Harbor后端存储的性能也会成为瓶颈。常见影响因素:layer数量:每个RUN指令都会产生一层,分层越多,元数据和上传请求越多。压缩完整性:默认docker push会对layer做压缩和校验,如果本地磁盘空间不足,压缩临时文件写不进去,就会出现莫名其妙的失败。网络质量:内网推大镜像通常比较快,跨公网推几百MB的镜像,就必须考虑重传和速率。如果实在很慢,可以先确认Harbor的存储后端类型。如果后端用的是远端对象存储,上传路径需要经过Harbor组件再转到对象存储,瓶颈往往在Harbor到对象存储这一段。这时候检查Harbor所在服务器的带宽、磁盘以及对象存储的访问延迟,比反复折腾docker配置更有效。5.2 推送中断后的恢复机制docker push在传输过程中中断,重新执行一次docker push并不是「从头传一遍」。Docker会复用本地已有的镜像layer,客户端也会跳过那些已经在目标仓库存在且digest一致的layer。所以推送中断后再执行一次,速度通常快很多。但要注意,这不是严格意义上的断点续传,它只适用于「本地和远端已同步的layer」。如果中断发生在某个layer上传到一半的时候,那部分数据会被丢弃,重新push时这个layer还要重传。在实际操作中,如果推送大镜像多次失败,一次一次手动push很痛苦。可以用一个简单的循环脚本把命令套起来:for i in $(seq 1 5); do docker push harbor.example.com/project/app:${BUILD_NUMBER} && break echo "retry $i..." sleep 5 done这里加睡眠时间,是为了避免在Harbor还未从上一个失败请求中恢复时,立刻发起重试导致压力叠加。5.3 磁盘空间与Harbor回收机制Harbor侧如果磁盘空间满了,一般的表现是push过程中突然返回500 internal server error,或者Harbor界面显示存储不可用。Harbor本身的存储分两部分:数据库和后端镜像存储(本地目录或对象存储)。镜像存储满,会影响push和pull,但Harbor的web页面可能还正常。Harbor有垃圾回收功能,清理被标记为删除但未被引用的blob。不过GC不是实时的,删除镜像后不能立刻腾出空间。如果生产环境空间告警,需要先检查未GC的blob,再配合运维流程清理,否则可能遇到「删除镜像后空间没变」的疑问。客户端本地也可能遇到磁盘空间不足。镜像构建和本地缓存占空间非常大,尤其是执行完多个项目的build后,/var/lib/docker可能已经撑得很满。push时docker需要处理layer压缩和临时文件,没有余量就会先报错的可能是本地,而不是Harbor。可以先执行docker system df查看docker磁盘占用分布。5.4 并发推送的额度与控制Harbor对推送请求会做一定的并发控制,尤其在后端存储较弱时。多个CI流水线同时往同一个项目推送,可能出现部分请求排队超时。个人经历是,Harbor节点上如果有多个生产项目同时发版,偶尔会出现connection reset by peer或者EOF,这往往不是Harbor完全不可用,而是瞬时压力大。控制并发推送的核心是错峰部署。如果避不开,建议在Harbor前面加限流或在脚本里控制并发数。另外一个容易被忽略的角度是:镜像的layer数量。尽量合并Dockerfile中的RUN指令,让镜像层数少一点,每个push需要的请求数就少,整体并发压力也小。6. 常见报错速查表与排查技巧下面这张表是我平时排障时最常用的对照表,覆盖了push场景中最常出现的几类报错:报错关键词常见原因排查方向401 Unauthorized凭证错误、凭证过期、机器人账号token失效、时间偏差重新login;检查账号权限;校对时间403 Forbidden账号没有目标项目的push权限检查项目成员角色;检查项目是否存在404 Not Found项目名错误或目标仓库不存在检查tag中的项目名;在Harbor界面对照x509: certificate证书不受信任、域名不匹配、证书过期配置CA证书;确认访问域名和证书SAN一致http response to HTTPS clientHarbor只提供HTTP,但docker默认走HTTPS配置insecure-registriesconnection reset by peer网络不稳定、Harbor压力大、防火墙reset检查网络和Harbor日志no space left on device本地docker目录或Harbor存储空间不足清理docker缓存;Harbor侧GC或扩容manifest invalid/unknown blob镜像层损坏、Harbor存储不一致重新构建镜像;检查Harbor存储后端denied: requested access to the resource is denied项目权限、tag不可变策略查项目权限和不可变规则6.1 401混杂着Hostname不匹配的情况有个场景容易被漏掉:Harbor配置的访问域名是harbor.example.com,但你在客户端用IP或者另一个别名去push。证书里的主机名和你实际访问的主机名不一致,导致即使成功login,push时依然失败或者被判定为认证失败。所以要么确保客户端访问地址与证书SAN一致,要么在证书里加上所有可能使用的域名和IP。6.2 Harbor日志在哪个环节看排查疑难报错,Harbor的容器日志是重要依据。Harbor部署方式是容器化,日志分散在各个容器里。一般可以看harbor-core容器日志(处理认证和API请求)以及harbor-registry容器日志(处理镜像blob上传):docker logs harbor-core --tail 200 docker logs harbor-registry --tail 200日志中出现authentication failed或storage driver相关字样时,方向就很清晰了。我用这个方式定位过几次connection reset问题,实际是Harbor所在机器到对象存储之间的网络抖动,从Harbor自己的应用日志里能看到上传超时的信息。6.3 排查顺序建议遇到push失败,不要一上来就盯着docker命令的输出反复看。我习惯按这个顺序排查:看网络层:curl -v <Harbor>/v2/确认连通和协议。看认证层:重新执行docker login并使用docker logout清理旧凭证后再测。看项目层:确认目标项目存在,权限到位。看配置层:daemon.json证书、insecure-registries配置。看服务层:Harbor自身存储、日志、后端依赖。到目前为止,遵循这个顺序,十次有九次能在五分钟内定位到问题。一些实际使用中的体会推送镜像这件事,次数多了,慢慢会形成自己的固定套路。我现在的习惯是:所有自动化脚本里,push前强制login并检查结果;tag一律用构建编号加commit短哈希,尽量避免可变的latest;推送完成后调用一次Harbor API确认digest和tag对应关系;遇到大镜像推送,先在本地确认磁盘余量再发起push,免得推到一半因为本地空间不足失败,白折腾时间。还有一个值得推荐的小技巧:在Harbor侧开启镜像复制和配额管理,配合项目级的清理策略,能有效避免「镜像积压太多导致存储告警」的被动局面。把push和pull的边界条件提前想清楚,远比每次出问题再去翻日志要高效。尤其当你管理多个项目、多个环境时,统一tag命名规范和权限模型,带来的收益非常明显。
前一阵子帮同事排查一个推送镜像失败的场景,docker login都成功了,Harbor页面也登录得上,偏偏docker push就报unauthorized。折腾一圈,根因是那台机器上docker客户端缓存了旧的凭证,重新执行login时又因为终端开了代理环境变量,请求打到了别的地址。这种问题在推送镜像到Harbor时特别典型。很多人都觉得docker push嘛,就是一条命令的事,但真正在CI/CD里跑起来,或者在跨网络环境手工推送的时候,你会发现它背后牵扯的是客户端配置、仓库权限、镜像命名、网络策略、证书信任、存储空间这一整套链路。这篇文章就把整个push链路上最容易出问题的点逐一拆开,按实际排查的顺序来讲,尽量把那些官方文档不会明说、但你必须知道的坑都点到位。1. 动手push前,先把这三件事看清楚1.1 镜像tag里的仓库地址,到底该怎么写有不少人第一次拿到Harbor的访问地址,直接拿web页面的URL去拼镜像tag,结果要么端口多写了,要么路径少写了一层。Harbor的镜像地址遵循的标准格式是:<Harbor主机地址>:<端口>/<项目名>/<镜像名>:<tag>注意几个容易写错的地方:端口不是必填项。如果Harbor通过80端口对外提供HTTP服务,那么harbor.example.com/library/demo:v1是合法的;如果端口是8443、30002这类非默认端口,则必须显式写进tag里,例如harbor.example.com:8443/library/demo:v1。项目名(project)这一层必须存在。Harbor和Docker Hub不同,Harbor里镜像必须归属到某个项目下。管理员如果开启了自动创建项目,你可能还能侥幸推送不存在的项目名,但正规生产环境一般都会关掉这个开关。项目不存在时,服务端直接拒绝,根本不会给你创建的机会。镜像名这一层只能使用小写字母、数字、下划线、点号和横线。大写字母或者斜杠以外的特殊字符都会触发校验错误,有的客户端会直接提示invalid reference format,有的会等到服务端才返回报错。1.2 项目是否存在,以及项目下的权限Harbor的项目类型分为公开和私有。公开项目意味着只要Harbor服务可达,任何人都可以拉取镜像,但推送仍然需要认证。私有项目则拉取和推送都需要登录。我建议大家在push前先确认一件事:目标项目的名字到底是不是你以为的那个。生产环境常见场景是:开发环境里项目叫demo-server,测试环境项目叫demo-server-test,因为环境不同导致tag前缀打错,最后镜像推进了错误的项目,回滚时才发现找不到正确的镜像版本。这种事真的不少见。如果项目不存在、或者你登录的账号没有该项目的push权限,服务端一般会返回403 forbidden。注意,有些Harbor版本会返回401 unauthorized,这是服务端为了防止探测项目信息,故意把「没权限」和「没登录」统一处理了。所以遇到401时,除了检查用户名密码,还要回头看下权限分配。1.3 网络连通性和出网策略推送前先确认客户端到Harbor的443/80端口是否可达。很多时候网络问题不是Harbor这边挂了,而是中间有防火墙、安全组或者代理干扰。这里有一个很典型的坑:公司内网机器如果设置了HTTP代理环境变量(http_proxy、https_proxy、no_proxy),docker客户端访问内网Harbor时默认也会走代理。如果代理服务器不能解析内网域名,或者代理策略拦截了内网地址,就会超时或报连接错误。正确的做法是在no_proxy里加上Harbor的域名和IP段,确保内网流量直连。另外,Harbor如果做了域名解析,而你本地/etc/hosts里写了测试解析,和实际线上IP不一致,也会出现奇怪的连接被拒。建议先用curl -v <harbor地址>/v2/验证连通性和协议,再执行docker命令。这样可以把网络层和docker客户端的问题分开排查。2. 登录认证体系,比表面上要复杂2.1 三种常见账号形态和适用场景Harbor的账号体系主要有三类:本地用户、管理员账号、机器人账号。日常开发中很多人喜欢直接用管理员账号或者某个共用账号登录,图省事,但这在稍微规范一点的团队里都是禁忌。账号类型适用场景权限范围本地用户开发人员手工推送、拉取按被加入的项目授权管理员账号平台初始化、系统配置、维护全平台权限,尽量少用机器人账号CI/CD流水线、自动化脚本仅限指定项目,可限定有效期尤其是CI/CD流水线,千万不要在构建脚本里写死一个本地账号的密码。一方面密码定期变更会导致流水线突然失败,另一方面密码会出现在日志、变量文件等地方,泄露风险大。Harbor的机器人账号是为此设计的:创建时为某个项目生成一段token,用户名形如robot$<机器人名称>,推送时直接当成账号和密码用。这里有个小细节容易坑到人:机器人账号的用户名里带了$符号,在bash脚本里如果不对$做转义,$robot会被当成Shell变量展开,实际发送的账号就变了。在命令行使用时一定要加引号:docker login harbor.example.com -u 'robot$deploy' -p 'xxxxxx'2.2 凭证缓存与登录状态失效docker login成功之后,凭证会写入客户端配置目录。Linux下默认是~/.docker/config.json,其中auths字段保存Base64编码的用户名密码。问题在于:如果机器上有新旧两套配置,或者你设置了DOCKER_CONFIG环境变量指向别的目录,可能登录的目标和实际读取的凭证不在一个地方。一旦密码或机器人token变更过,旧的凭证还留在config.json里,docker login虽然能覆盖,但你如果没有重新执行,下一次push读到旧凭证就会401。在共享的构建机上,还可能存在多个用户使用同一套docker配置的情况,互相覆盖凭证,导致「刚才明明能登录,怎么过会儿就不行了」。我的建议是:自动化脚本里每次push前强制执行一次docker login,并检查退出码。不要依赖「之前已经登录过了」,因为凭证状态是不可信的。登录时可以使用--password-stdin避免密码出现在进程列表里:echo "$HARBOR_ROBOT_TOKEN" | docker login harbor.example.com -u 'robot$deploy' --password-stdin2.3 时间同步对认证的影响这个坑隐蔽性极高。Harbor在鉴权时会校验时间窗口,如果客户端主机、Harbor服务器或者后端认证服务的系统时间不一致,即使账号密码完全正确,也会出现401或token解析失败。很多虚拟机环境的时间同步策略不严格,开机几个月后时间偏差达到几分钟很常见。遇到诡异的认证失败问题,先看一眼date,再做时间同步,往往几十秒就解决了。3. 镜像命名、tag与仓库规则3.1 为什么push前常常要先docker tag很多人搞不清楚什么时候需要docker tag,总觉得docker push就能直接换仓库名。实际上,docker push的语义是「把本地已有的某个tag所指向的镜像,推到该tag指定的仓库地址」,它不会帮你自动改名。如果本地镜像名是demo-server:1.0.0,而你希望推送到Harbor里的library/demo-server:1.0.0,就必须先执行:docker tag demo-server:1.0.0 harbor.example.com:8443/library/demo-server:1.0.0这个操作并不会重新打包镜像,它只是给同一个镜像增加了一个引用名,类似文件系统里的硬链接。本地磁盘占用不会翻倍,可以放心操作。在CI场景里,我经常看到流水线里构建完镜像后输出的名称,和实际docker push使用的名称不一致,导致镜像推到了别的仓库或者漏推了。一个有效的做法是在流水线一开始就统一用完整仓库名作为镜像名来构建,比如docker build -t harbor.example.com:8443/project/app:${BUILD_NUMBER} .,后面直接push,省掉中间的tag步骤,也少一层出错机会。3.2 tag不可变与重复推送生产Harbor往往开启了「Tag不可变」策略,防止同名tag被恶意或误操作覆盖。这时候如果你尝试推送一个已经存在且不可变的tag,服务端会返回denied或405 method not allowed。需要注意,这个策略是项目级别的,不同项目可以配置不同规则。CI里如果每次都用latest作为tag,并且Harbor开启了不可变,第二次构建就会直接失败。更合理的做法是:固定版本:app-${BUILD_NUMBER}-${GIT_COMMIT_SHORT},每次构建生成唯一tag。稳定性版本:app-${MAJOR}.${MINOR}.${PATCH},仅在发布时更新。业务标签:latest、stable,通过运维平台或自动化任务在发版后更新。这种tag策略能避免「同名tag指向不同镜像」带来的环境一致性灾难,也便于按版本号回滚。3.3 跨仓库复制可能带来的digest混乱Harbor自带复制规则,可以把某个项目下的镜像自动同步到另一个Harbor实例或项目下。被复制过来的镜像虽然能拉取,但它和源镜像的存储结构不一定完全一致,尤其是在跨Harbor实例、后端存储类型不同的情况下。如果你在push时遇到manifest already exists这类提示,先别急着以为镜像推上去了。建议push完成后立即用docker manifest inspect或Harbor API核对一下目标tag的digest,确保实际内容是自己构建的那一个。4. 客户端侧配置:证书和daemon.json4.1 HTTPS、HTTP和insecure-registriesHarbor生产环境基本都是HTTPS,但个人测试环境、内网环境里以HTTP方式跑的场景也不少。Docker客户端默认在后端使用HTTPS协议访问仓库,如果Harbor只支持HTTP,而你直接docker push,会出现类似http: server gave HTTP response to HTTPS client的报错。解决办法是在Docker守护进程配置里,把该Harbor地址加入insecure-registries。Linux下编辑/etc/docker/daemon.json:{ "insecure-registries": [ "harbor.example.com", "harbor.example.com:80" ] }改完必须重启docker守护进程:sudo systemctl restart docker需要注意的是,insecure-registries的含义是「允许对该地址使用HTTP或跳过证书校验」,并不是「只对该地址使用HTTP」。如果你配置的地址是harbor.example.com,同时Harbor也在80端口提供HTTP,push时docker会先尝试HTTPS,不行再退回HTTP。为了减少不必要的重试和日志误导,最好把带端口的地址和不带端口的地址都写清楚,并保证配置内容一致。4.2 私有CA证书的正确配置位置很多企业内部Harbor使用的是自签CA或私有CA签发的证书。在这种情况下,即使你在insecure-registries里加了地址,docker login也可能成功,但push时依旧报证书校验错误。原因在于docker push时它会更严格地校验TLS证书链,跳过校验的配置未必对每个操作都生效。更规范的做法是把Harbor使用的CA根证书,放到docker客户端信任目录下:/etc/docker/certs.d/<Harbor地址>:<端口>/ca.crt例如Harbor地址是harbor.example.com:8443,那么目录和文件名就是:/etc/docker/certs.d/harbor.example.com:8443/ca.crt注意,目录名中的端口只有在端口非443时才需要写,443时可以省略。配置完成后同样重启docker。这样配置的好处是,不用为了跳过校验而牺牲TLS安全性,同时也能避免某些镜像仓库对「跳过证书校验」的额外限制。4.3 Windows和macOS Docker Desktop的特殊性如果你在Windows或macOS上用Docker Desktop,daemon.json的位置和Linux不一样。Docker Desktop提供了图形界面叫Docker Engine,可以直接编辑配置JSON,它实际管理的还是一个daemon.json,但路径在虚拟机的内部。界面修改完点Apply就会生效。另外,Windows下走PowerShell推送时,记得检查代理设置和网络配置文件,尤其是公司域控环境下,系统代理可能自动注入,导致docker命令访问的地址不是预期目标。4.4 重启docker守护进程带来的副作用修改了daemon.json之后要重启docker,这个操作会把运行中的容器全部停掉。如果是生产机器上正在跑业务容器,重启前一定要提前规划迁移或发布窗口。很多同学为了省事,push前临时在daemon.json里加insecure-registries,然后重启,把正在跑的服务搞挂了,这就属于典型的低级事故。5. 推送过程本身的性能、分块与稳定性问题5.1 大镜像推送为什么那么慢单个镜像大的时候,docker push会把镜像拆成多个layer分块上传。Docker客户端默认会开启并行上传,但并行度受配置影响,同时Harbor后端存储的性能也会成为瓶颈。常见影响因素:layer数量:每个RUN指令都会产生一层,分层越多,元数据和上传请求越多。压缩完整性:默认docker push会对layer做压缩和校验,如果本地磁盘空间不足,压缩临时文件写不进去,就会出现莫名其妙的失败。网络质量:内网推大镜像通常比较快,跨公网推几百MB的镜像,就必须考虑重传和速率。如果实在很慢,可以先确认Harbor的存储后端类型。如果后端用的是远端对象存储,上传路径需要经过Harbor组件再转到对象存储,瓶颈往往在Harbor到对象存储这一段。这时候检查Harbor所在服务器的带宽、磁盘以及对象存储的访问延迟,比反复折腾docker配置更有效。5.2 推送中断后的恢复机制docker push在传输过程中中断,重新执行一次docker push并不是「从头传一遍」。Docker会复用本地已有的镜像layer,客户端也会跳过那些已经在目标仓库存在且digest一致的layer。所以推送中断后再执行一次,速度通常快很多。但要注意,这不是严格意义上的断点续传,它只适用于「本地和远端已同步的layer」。如果中断发生在某个layer上传到一半的时候,那部分数据会被丢弃,重新push时这个layer还要重传。在实际操作中,如果推送大镜像多次失败,一次一次手动push很痛苦。可以用一个简单的循环脚本把命令套起来:for i in $(seq 1 5); do docker push harbor.example.com/project/app:${BUILD_NUMBER} && break echo "retry $i..." sleep 5 done这里加睡眠时间,是为了避免在Harbor还未从上一个失败请求中恢复时,立刻发起重试导致压力叠加。5.3 磁盘空间与Harbor回收机制Harbor侧如果磁盘空间满了,一般的表现是push过程中突然返回500 internal server error,或者Harbor界面显示存储不可用。Harbor本身的存储分两部分:数据库和后端镜像存储(本地目录或对象存储)。镜像存储满,会影响push和pull,但Harbor的web页面可能还正常。Harbor有垃圾回收功能,清理被标记为删除但未被引用的blob。不过GC不是实时的,删除镜像后不能立刻腾出空间。如果生产环境空间告警,需要先检查未GC的blob,再配合运维流程清理,否则可能遇到「删除镜像后空间没变」的疑问。客户端本地也可能遇到磁盘空间不足。镜像构建和本地缓存占空间非常大,尤其是执行完多个项目的build后,/var/lib/docker可能已经撑得很满。push时docker需要处理layer压缩和临时文件,没有余量就会先报错的可能是本地,而不是Harbor。可以先执行docker system df查看docker磁盘占用分布。5.4 并发推送的额度与控制Harbor对推送请求会做一定的并发控制,尤其在后端存储较弱时。多个CI流水线同时往同一个项目推送,可能出现部分请求排队超时。个人经历是,Harbor节点上如果有多个生产项目同时发版,偶尔会出现connection reset by peer或者EOF,这往往不是Harbor完全不可用,而是瞬时压力大。控制并发推送的核心是错峰部署。如果避不开,建议在Harbor前面加限流或在脚本里控制并发数。另外一个容易被忽略的角度是:镜像的layer数量。尽量合并Dockerfile中的RUN指令,让镜像层数少一点,每个push需要的请求数就少,整体并发压力也小。6. 常见报错速查表与排查技巧下面这张表是我平时排障时最常用的对照表,覆盖了push场景中最常出现的几类报错:报错关键词常见原因排查方向401 Unauthorized凭证错误、凭证过期、机器人账号token失效、时间偏差重新login;检查账号权限;校对时间403 Forbidden账号没有目标项目的push权限检查项目成员角色;检查项目是否存在404 Not Found项目名错误或目标仓库不存在检查tag中的项目名;在Harbor界面对照x509: certificate证书不受信任、域名不匹配、证书过期配置CA证书;确认访问域名和证书SAN一致http response to HTTPS clientHarbor只提供HTTP,但docker默认走HTTPS配置insecure-registriesconnection reset by peer网络不稳定、Harbor压力大、防火墙reset检查网络和Harbor日志no space left on device本地docker目录或Harbor存储空间不足清理docker缓存;Harbor侧GC或扩容manifest invalid/unknown blob镜像层损坏、Harbor存储不一致重新构建镜像;检查Harbor存储后端denied: requested access to the resource is denied项目权限、tag不可变策略查项目权限和不可变规则6.1 401混杂着Hostname不匹配的情况有个场景容易被漏掉:Harbor配置的访问域名是harbor.example.com,但你在客户端用IP或者另一个别名去push。证书里的主机名和你实际访问的主机名不一致,导致即使成功login,push时依然失败或者被判定为认证失败。所以要么确保客户端访问地址与证书SAN一致,要么在证书里加上所有可能使用的域名和IP。6.2 Harbor日志在哪个环节看排查疑难报错,Harbor的容器日志是重要依据。Harbor部署方式是容器化,日志分散在各个容器里。一般可以看harbor-core容器日志(处理认证和API请求)以及harbor-registry容器日志(处理镜像blob上传):docker logs harbor-core --tail 200 docker logs harbor-registry --tail 200日志中出现authentication failed或storage driver相关字样时,方向就很清晰了。我用这个方式定位过几次connection reset问题,实际是Harbor所在机器到对象存储之间的网络抖动,从Harbor自己的应用日志里能看到上传超时的信息。6.3 排查顺序建议遇到push失败,不要一上来就盯着docker命令的输出反复看。我习惯按这个顺序排查:看网络层:curl -v <Harbor>/v2/确认连通和协议。看认证层:重新执行docker login并使用docker logout清理旧凭证后再测。看项目层:确认目标项目存在,权限到位。看配置层:daemon.json证书、insecure-registries配置。看服务层:Harbor自身存储、日志、后端依赖。到目前为止,遵循这个顺序,十次有九次能在五分钟内定位到问题。一些实际使用中的体会推送镜像这件事,次数多了,慢慢会形成自己的固定套路。我现在的习惯是:所有自动化脚本里,push前强制login并检查结果;tag一律用构建编号加commit短哈希,尽量避免可变的latest;推送完成后调用一次Harbor API确认digest和tag对应关系;遇到大镜像推送,先在本地确认磁盘余量再发起push,免得推到一半因为本地空间不足失败,白折腾时间。还有一个值得推荐的小技巧:在Harbor侧开启镜像复制和配额管理,配合项目级的清理策略,能有效避免「镜像积压太多导致存储告警」的被动局面。把push和pull的边界条件提前想清楚,远比每次出问题再去翻日志要高效。尤其当你管理多个项目、多个环境时,统一tag命名规范和权限模型,带来的收益非常明显。