news 2026/10/17 19:32:33

Docker私有镜像仓库搭建实战:从Registry到Harbor的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker私有镜像仓库搭建实战:从Registry到Harbor的完整指南

要说今年最让我没脾气的场景,不是服务挂了,而是朋友那边部署新环境,拉一个基础镜像花了大半天。屏幕上转圈转了十分钟,进度条一动不动,问他网络,说都正常。后来我说,要不自己搭个私有镜像仓库,先把常用镜像推上去,以后不管是新环境部署还是内网分发,都从私服拉。他半信半疑,直到我们把第一批镜像传到内网仓库之后,再开一台全新机器,几十秒就完成“拉基础镜像→启动容器”这一步,他才明白这件事该早做。

这篇就是把我从零搭建 dockerhub 私服(本质上就是自建镜像仓库)的过程、踩过的坑、以及后面用了一年总结出来的排错经验全写出来。覆盖面包括最基础的 Registry 方案、HTTPS 证书配置、账号认证、常见报错排查链路,以及往 Harbor 迁移时的体验差异。不管你是第一次接触私有仓库,还是已经搭过但被证书、磁盘、权限问题折磨过,应该都能找到对应的解法。

1. 先想清楚再动手:私服到底解决了什么问题

1.1 不是每一个环境都能“秒连官方源”

先说背景。很多团队初期不搭私服,觉得直接从公共镜像源拉就行,省事。但实际跑起来,问题集中在几个地方:

  • 新机器初始化环境时,基础镜像反复从远端下载,耗时很长,尤其是代理机、离线机房、安全隔离区这类网络受限的环境。
  • 构建机需要同时拉很多镜像,公共源偶尔限速,导致流水线排队时间不可控。
  • 团队内部发布的应用镜像散落在各台构建机上,缺少统一集中管理的地方。
  • 安全审计要求镜像来源可追溯,公共源上随便拉一个仓库名,都不知道背后是谁维护的。

这种情况下,私有仓库就不是“备胎”了,而是整个镜像供应链的核心节点。所有镜像先推送到私服,再由私服统一对外提供分发,无需每个人都直接访问外部镜像源。网络开销缩小,速度提升,安全性和可控性也上了一个台阶。

1.2 自建仓库的常见方案分层

我把当前常见的“dockerhub 私服”方案归纳成三个梯度,方便你根据团队规模选型:

方案复杂度适合场景典型产品
轻量级 Registry低个人、小团队、临时测试Docker Registry v2
完整企业级仓库中高多团队、需要镜像回收和项目权限Harbor
商业存储适配不稳定已有对象存储、需要跨机房分发各种云原生制品库

如果只是一台服务器、两三个人用,其实 Docker Registry 镜像就够了,五秒钟就能起一个能用的仓库。如果涉及权限细分、镜像保留策略、漏洞扫描,那就直接上 Harbor,没必要自己写管理端。

1.3 别指望自建仓库能完全替代“缓存加速”

有一个很重要的事实要先说明:自建私服不等于镜像加速器。它解决的是“镜像分发可控”的问题,但私有服务器访问远端源(比如公共 Docker Hub)的速度,依然取决于服务器所在网络。如果你墙内服务器访问外部源龟速,那么私服拉远端镜像也一样龟速。

所以我会用比较务实的组合拳解决:搭建私有仓库 + 配置国内可用的镜像加速器(或者在网络较好的环境里先同步好镜像)。这篇文章的核心还是私服本身,但涉及拉外部镜像的步骤会夹带这个思路。需要强调,整个过程都是通过正规配置代理镜像源、DNS、镜像加速等方式实现,尽量不依赖脆弱链路。

2. 三十分钟跑通一个能用的私有仓库

2.1 硬件和操作系统准备

我搭私服用的是一台四核八线程、16GB 内存的机器,硬盘 1TB 的纯 SSD。说实话,这个配置对初期团队来说已经偏剩,如果只是给十来个人用,2 核 4G 加 500GB 空间都能跑得很顺。镜像仓库本质是一个存储 + 索引服务,限制它上限的主要是磁盘 IO 和网络带宽,CPU 要求并不高。

存储路径上,我会单独留出一个数据分区挂载给 Registry 容器,避免系统盘写满影响整机。我的目录规划是这样:

/data/registry-data # 镜像数据存储目录 /data/certs # HTTPS 证书目录 /data/auth # 认证文件目录

有人说直接放 /opt 下也行,但对于镜像这个“越积越肥”的东西,独立分区是很有必要的,后面垃圾回收、磁盘扩容都方便。

2.2 启动一个最简 Registry 容器

先不聊复杂配置,用最小命令跑一个 Registry:

docker run -d --name registry \ -p 5000:5000 \ -v /data/registry-data:/var/lib/registry \ --restart=always \ registry:2.8.3

这里官方镜像建议不省略 tag,避免以后升级行为变化影响线上仓库。此时你已经有一个能用的仓库了,注册中心默认没有认证,也没有 HTTPS。如果你的环境只是内网临时验证,那可以直接用。但千万别把这种裸裸的 5000 端口直接暴露到公网,Registry 本身没有内置的认证机制,别人知道地址就能往里推镜像。

注意:官方 Registry 老版本默认REGISTRY_STORAGE_DELETE_ENABLED=false,意味着你调用 Delete API 删除镜像不会被物理清理。如果你想在测试环境开启删除能力,需要给容器加环境变量-e REGISTRY_STORAGE_DELETE_ENABLED=true,否则一开始删不掉还会误以为是自己操作问题。

2.3 给镜像打上能进仓库的 tag

仓库起来后,想把某台机器上的镜像推进去,第一个容易出错的点是 tag 格式。私有仓库的镜像全名必须是仓库地址/镜像名:标签的形式。比如本机有一个web-server:latest镜像,需要先打 tag:

docker tag web-server:latest localhost:5000/web-server:v1.0.0 docker push localhost:5000/web-server:v1.0.0

这里localhost:5000在 SSH 登录的服务器上执行没问题;但在开发机推送时,应该换成服务器实际可达的 IP 或域名,例如192.168.10.20:5000。

如果你直接docker push localhost:5000/web-server:latest,在另一台开发机上想要拉这个镜像,肯定找不到,因为开发机上的 localhost 是它自己。这属于 tag 书写意识的问题,排查时要先确认。

2.4 为什么我推荐固定镜像标签而不是 latest

初期图省事,大家都喜欢推latest,结果没过多久就出问题:明明构建机重新 build 并推送了,线上拉到的却是旧的。这是因为 Docker 默认的拉取策略,本地已有 latest 就不会主动远程拉新。这个问题会让人非常困惑,也不好排查。

我后来强制规定,所有进入私服的镜像必须打语义化版本标签,例如app-api:1.2.3,然后部署脚本里也使用精确版本。线上需要更新时,改变部署文件里的版本号再滚动更新。这样至少保证“每次都拿到和标签匹配的内容”,不会被缓存坑。

3. HTTPS 证书这一步,才是私服能不能落地的分水岭

3.1 为什么裸 HTTP 仓库没法舒服地跨机器用

默认情况下,Docker 客户端只信任 HTTPS 仓库,看到 HTTP 仓库时会直接拒绝,除非你在每台客户端的/etc/docker/daemon.json上的insecure-registries里填写这个仓库地址。这看似是个小配置,实际体验却很糟糕:

  • 每台需要用这个仓库的机器都要改 daemon.json,改完还要重启 Docker 守护进程。
  • 一旦仓库换了域名或 IP,就要去每台机器上同步修改列表。
  • 有安全规范的环境里,明文 HTTP 推送镜像本身就过不了合规。

所以为了后续不挨个机器改配置,我优先解决证书问题。

3.2 自签证书的完整生成步骤

如果你有正式域名,且可以免费申请正规 HTTPS 证书,那最省事。但很多团队自建仓库只是用内网域名或 IP,没有公网证书。此时可以用自签证书。

我用 OpenSSL 生成 CA 和服务器证书,命令如下:

mkdir -p /data/certs && cd /data/certs # 生成 CA 私钥 openssl genrsa -out ca.key 2048 # 生成 CA 自签证书 openssl req -x509 -new -nodes -key ca.key -days 3650 \ -subj "/CN=Docker Private CA" -out ca.crt # 生成服务器私钥 openssl genrsa -out server.key 2048 # 创建签名请求 openssl req -new -key server.key \ -subj "/CN=hub.internal.example.com" \ -out server.csr

接下来要写一个扩展文件,把后面的域名和 IP 都填进去,否则浏览器或某些客户端校验会不通过:

cat > ext.cnf <<EOF subjectAltName = DNS:hub.internal.example.com, IP:192.168.10.20 extendedKeyUsage = serverAuth EOF openssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -days 825 -out server.crt -extfile ext.cnf

这里有三个细节容易被忽略:

  • 扩展字段里一定要包含所有会用到的 IP 和域名,缺一个,客户端访问时就报证书验证失败。
  • 证书有效期不要太短,自签场景下的证书是 DevOps 手工维护的,忘了续期会导致所有新机器拉取失败。825 天是常见上限,不少客户端的兼容性上限也在 825 天左右。
  • 不要把CN和 SAN 地址填成两个不同的东西,否则某些严格客户端会直接报错。

3.3 把证书挂进 Registry 容器

生成证书后,用挂载方式把它带进 Registry:

docker run -d --name registry \ -p 443:5000 \ -v /data/registry-data:/var/lib/registry \ -v /data/certs:/certs \ -v /data/auth:/auth \ -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt \ -e REGISTRY_HTTP_TLS_KEY=/certs/server.key \ -e REGISTRY_STORAGE_DELETE_ENABLED=true \ --restart=always \ registry:2.8.3

我把容器内的 5000 端口映射到宿主机的 443 端口,这样访问地址就是https://hub.internal.example.com,省得每次都带:5000。这个映射对统一发布地址挺重要的,对用户来说,镜像仓库的 URL 越简单越好。

3.4 客户端信任 CA 的操作方式

要让每台客户端信任这个自签证书,不是简单加insecure-registries。正规做法是把 CA 证书复制到对应操作系统可信根证书目录:

# 以常见的 CentOS/RHEL 系为例 sudo cp ca.crt /etc/pki/ca-trust/source/anchors/ sudo update-ca-trust

Debian/Ubuntu 则是:

sudo cp ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates

然后确认/etc/docker/daemon.json里不需要写insecure-registries,直接重启 Docker:

sudo systemctl restart docker

这样做的好处是全局可信,不仅 Docker 能用,其他需要访问仓库的工具也能共享信任链。我在实际项目中,宁愿多花五分钟部署 CA,也不愿意直接在 daemon.json 里塞 insecure 列表。后者虽然快,但后续扩展到 K8s 节点时,每个节点都要维护一份名单,极易漏配。

4. 从裸奔到带锁:给私有仓库加账号认证

4.1 用 htpasswd 生成本地认证文件

Registry 本身不提供用户注册,但支持基于 HTTP Basic Auth 的认证。我在/data/auth目录下用 htpasswd 生成密码文件:

mkdir -p /data/auth htpasswd -Bc /data/auth/htpasswd opsadmin

会交互式要求输入密码。如果要追加用户,去掉-c参数:

htpasswd -B /data/auth/htpasswd developer01

-B表示强制使用 bcrypt 加密算法,Docker 官方文档强烈推荐这种方式,不要用旧版 md5。生成后,检查文件格式确认存在一个用户一行,符合预期。

4.2 让 Registry 开启认证并配合 Nginx 反代

Registry 容器里挂载/data/auth目录,并添加两个环境变量:

docker run -d --name registry \ -p 127.0.0.1:5000:5000 \ -v /data/registry-data:/var/lib/registry \ -v /data/certs:/certs \ -v /data/auth:/auth \ -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt \ -e REGISTRY_HTTP_TLS_KEY=/certs/server.key \ -e REGISTRY_AUTH=htpasswd \ -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \ -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ -e REGISTRY_STORAGE_DELETE_ENABLED=true \ --restart=always \ registry:2.8.3

为了更方便做 TLS 终止和其他安全配置,我的服务器上还用了 Nginx 反代,把对外的 443 流量转发到内网 Registry 的 5000。Nginx 配置关键部分如下:

server { listen 443 ssl; server_name hub.internal.example.com; ssl_certificate /data/certs/server.crt; ssl_certificate_key /data/certs/server.key; client_max_body_size 2g; location / { proxy_pass http://127.0.0.1:5000; 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; } }

切断 5000 端口对外暴露后,外部只能走带 TLS 的 443。client_max_body_size必须设置够大,否则推镜像时 Nginx 会返回 413 Request Entity Too Large,这个坑很典型。

4.3 docker login 常见的 401/403 排查思路

开启认证后,正常拉取镜像前需要先登录:

docker login hub.internal.example.com

可能遇到的问题,我按出现频率排一下:

  • 输入正确的用户名密码却报 401:检查 htpasswd 文件是否放在容器内正确路径,注意容器重启后挂载目录是否会替换掉原路径。
  • 推镜像时报 403:先看目标仓库是否存在。Registry 默认开放匿名读,但匿名写会被拒。确保已经登录且用户名有权限。
  • 登录后马上又失效:检查客户端和服务端的系统时间是否一致,证书时间对不上也会触发异常安全校验。

认证体系建议小步走:先验证本机curl -u user:pass https://hub.internal.example.com/v2/能通,再走 docker login。这样可以把“网络链路问题”和“Registry 认证问题”切成两段去判断。

5. 用了一年,我整理出的常见问题排查链路

5.1 拉镜像超时,不一定是网络问题

先看一个现象:执行docker pull hub.internal.example.com/app:1.0,卡了很久然后报dial tcp: lookup hub.internal.example.com: no such host。这是 DNS 没解析到。但还有一种非常隐蔽的超时,DNS 解析正常,TCP 也能连上,数据却一直拉不动。

这时我会先在能正常访问仓库的机器上执行:

curl -v https://hub.internal.example.com/v2/

如果返回正常的{}或认证提示,说明仓库本身没问题。再看客户端这边,很多情况是公司内网防火墙,拦截了对仓库上大文件的下载。排查手法也很简单:拉一个小镜像测试是否通畅;再拉一个 200MB 以上的镜像测试,如果小的能通、大的卡死,大概率是流量策略问题,而不是 Registry 配置问题。

5.2 push 报错 405 Method Not Allowed,可以往删缓存方向查

有一次同事 push 镜像,进度条很快走完,突然报 405。这个问题容易被误判成认证问题或权限问题,但我观察下来,最常出现在 Nginx 反代已经去掉头部、转发目标变成“只读模式”的时候。

Registry 支持通过配置管理只读状态,常见配置项如:

auth: htpasswd: realm: basic-realm path: /auth/htpasswd storage: delete: enabled: true

如果 team 用配置管理工具统一下发,某个配置把delete.enabled隐藏或覆盖了,推送时就会出现 405。排错顺序建议是:

  1. 确认配置文件里 deletion 没有 false。
  2. 确认是否有镜像仓库页面开启了“禁止推送”策略。
  3. 确认 Nginx 反代层没有把PUT请求意外改写。

5.3 磁盘满了,Registry 打印不停流转

镜像仓库最经典问题就是磁盘爆掉。我见过最严重的一次,/data 分区 100%,Docker 守护进程和 Registry 日志疯狂刷no space left on device。这时别说推镜像,连重启 Registry 都可能失败。

解决思路分三个步骤:

第一步,先看大文件。有时候是容器日志,不是仓库数据:

du -sh /var/lib/docker/containers/*/

第二步,如果确实是/data/registry-data占满,可以启用 Registry 的垃圾回收:

docker exec registry registry garbage-collect --delete-untagged=true /etc/docker/registry/config.yml

这命令会把不再被任何清单引用的层和镜像删除。要特别留意的是,执行垃圾回收会让仓库进入只读状态,期间写入操作会失败。最好安排在业务低谷期执行。

第三步,ar 如果回收后空间仍然吃紧,说明 tag 保留策略没做好。需要结合 Harbor 或外部脚本做镜像数量上限清理,不能靠纯手工。

5.4 一个容易被忽略的事:inode 满了

有一次分区df -h显示只用了 60%,但是拉取镜像失败,系统报No space left on device。我用df -i一看,inode 已经耗尽。因为仓库里存了海量小标签和碎片文件,元数据节点先撑爆了。

这种情况光删镜像没用,需要把垃圾层清掉,甚至重新格式化仓库数据目录。我后来会在监控上同时加磁盘空间和 inode 使用率两项指标,避免再次被偷袭。

5.5 并发拉取镜像打满带宽后的连锁反应

团队从几十人扩张到一百多人的时候,内网同时有大量构建任务,私服带宽被打满,所有拉镜像操作都变慢。这个问题的本质是部署网络拓扑缺乏分流层。

我在私服前面加了简单的带宽缓存机制,常用镜像在构建机本地留存,构建时优先复用本地缓存;同时限制每个客户端的并发拉取数,避免单台机器一次性拉十几个镜像占完带宽。Docker 客户端可以通过--max-concurrent-downloads参数调节并发,配合调度系统能明显缓解带宽压力。

6. 如果 Registry 不够用,Harbor 是更舒服的升级方向

6.1 到了多团队协作阶段,Registry 的痛点会越来越明显

单用 Registry 时,所有用户共用一个账号,日志不区分人;镜像删除后的垃圾清理也要手工操作;没有 Web 界面,想看看仓库里有几个项目,只能靠 curl API 或装额外工具。这时候上 Harbor 是比较顺理成章的选择。

Harbor 本质上是在 Registry 之上封装了认证、项目隔离、WebUI、垃圾回收、漏洞扫描等能力。部署好之后,开发者面前多了一个可视化管理入口,不再只有漆黑的命令行.

6.2 安装 Harbor 的几个关键步骤与坑

Harbor 的安装包自带 docker-compose 编排,流程上比较清晰:下载 release 包 -> 修改 harbor.yml -> 执行 prepare -> 启动。

安装前建议用一台干净的机器跑,因为它会直接占用部分 Docker 资源,和已有 Registry 混在一起,容易造成端口冲突和存储互相干扰。

我踩过的坑主要有三个:

  • harbor.yml 里的 hostname 必须是客户端实际访问的域名,不能随手填一个localhost,否则 WebUI 登录没问题,但 Docker 客户端 push/pull 会解析错地址。
  • 默认配置是 HTTP 模式,需要把https部分的证书路径改成自己的证书,并关闭 HTTP 入口,否则对外发布时端口和协议都很混乱。
  • 安装目录尽量独立,不要放在系统 root 分区下,Harbor 自带的数据库和镜像数据体积增长很快。

6.3 Harbor 的镜像垃圾回收比 Registry 优雅很多

Harbor 在界面上可以直接设置垃圾回收任务,还可以设置策略:只保留最近 X 个版本、自动清理无 tag 镜像等。相比 Registry 手工跑命令,少了误操作风险。

但要注意,Harbor 底层依然是 Registry 的 storage 格式,所以底层磁盘监控不能少,界面上回收垃圾不会 100% 释放空间,它只是清理元数据和未被引用的层。最终系统层面还是要看分区实际占用。

6.4 从 Registry 迁移到 Harbor 的平替思路

如果你已经有一段很长的历史镜像在 Registry 里,想迁到 Harbor,有两个可行办法:

  • 直接把/data/registry-data的存储目录整体迁移到 Harbor 使用的数据卷,注意目录结构兼容性。
  • 用工具逐个仓库 pull 下来再 push 到 Harbor,慢但可控。

我建议小团队直接选第二种,顺便借此机会梳理一遍镜像清单,把没必要保留的旧版本清掉,迁移完的仓库更健康。如果你是几百个仓库的大规模场景,再考虑存储层直接挂载迁移。

7. 最后补几句维护私服的习惯

这类服务低调到几乎没存在感就是最好的状态,但越低调越容易被遗忘维护。下面几条是经历了故障之后才养成的习惯。

第一,给私服磁盘挂上监控,同时看空间使用率和 inode 使用率,两条指标缺一不可。很多人只看空间,踩过 inode 爆掉的坑才反应过来。

第二,定期执行垃圾回收。我都习惯把它放到每周六凌晨跑,期间跳过发布窗口,避免写入冲突。每次跑之前先做一次网络和存储状态的快照。

第三,维护一份镜像白名单。像基础镜像、中间件镜像、线上应用镜像,按不同的前缀放到不同项目空间下,避免所有人乱推,最后仓库变成一个版本垃圾桶。

第四,别把/data/registry-data放在临时目录或换机器时才想起来迁数据。它在镜像仓库体系里和代码仓库一样重要,丢失后虽然有 tag 列表但历史元数据恢复起来很痛苦。

如果只想抓到几条自己适用的经验,那我就说一句:证书的坑必须一次性做对,镜像标签不要全用 latest,磁盘监控要提前配好。这三个地方往往是私有仓库从“能用了”走向“很好用”的分水岭。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 19:31:26

Kafka接入AI实战:从消息管道搭建到Lag排查与重复消费

最近我花了整整一周时间&#xff0c;把手里两条核心业务链路彻底改造成了走 Kafka 的消息管道&#xff0c;其中一条已经正式接入 AI 推理服务。说句实话&#xff0c;刚开始我以为这活很简单&#xff1a;建个 topic、写个 producer、consumer 消费完丢给模型就完事了。真正跑起来…

作者头像 李华
网站建设 2026/10/17 19:30:33

Java实现可运行的递归下降分析器:从文法改造到语法解析

简介&#xff1a;本资源是面向计算机专业本科生与编译原理初学者的实验教学材料&#xff0c;聚焦自上而下的语法分析核心实践——递归下降分析器的设计与实现。内容涵盖LL(1)文法改造&#xff08;含左递归消除&#xff09;、FIRST/FOLLOW集手工推导与验证、词法分析器扩展&…

作者头像 李华
网站建设 2026/10/17 19:27:35

SQL Server 2008误删恢复:简单模式下无备份数据抢救方案

简介&#xff1a;本资源是一份面向SQL Server数据库管理员与运维工程师的实战型数据恢复指南&#xff0c;聚焦SQL Server 2008环境下误删除数据的紧急抢救方案。内容系统梳理了基于事务日志恢复的三大典型场景&#xff1a;当具备完整备份且恢复模式为“完全&#xff08;Full&am…

作者头像 李华
网站建设 2026/10/17 19:27:29

用友NC65安装全流程:Oracle表空间与数据源配置实战

简介&#xff1a;这是一份面向用友NC65及NCC高版本实施顾问的完整安装操作手册&#xff0c;专为自学搭建测试环境的新手设计&#xff0c;能解决官网资料零散、上手门槛高的问题。资源为单个doc文档&#xff0c;压缩包仅933KB&#xff0c;便于携带和查阅&#xff0c;内容覆盖从O…

作者头像 李华
网站建设 2026/10/17 19:24:31

用CCR路由让Claude Code一键切换OpenRouter、GLM和Kimi,彻底告别配额焦虑

最近在用一个挺有意思的组合&#xff1a;Claude Code 作为终端编程入口&#xff0c;外加一套叫 CCR 的路由配置&#xff0c;把我的请求分发到 OpenRouter、GLM 和 Kimi 三个不同的模型服务上。折腾完以后最大的感受是&#xff0c;以前那种“一个订阅、一个模型、打满全场”的做…

作者头像 李华