要说 Docker 用得久了,一定会遇到一个问题:镜像从哪来、推到哪去。如果只是本地开发,docker pull一下官方镜像还算舒服;可一旦你开始建设环境、对接测试、部署上线,没有自己的 docker 仓库,整套流程就会像没有仓库的物流公司,全靠手工搬运。这个主题我压了很久没写,因为“仓库”两个字看似简单,实际拆开之后涉及的东西不少:公有的 Docker Hub、轻量的 Registry、企业级的 Harbor,三者的定位、用法、坑都完全不一样。这篇文章就把这三条路线串起来讲清楚,从原理到落地的完整过程都覆盖,适合已经会docker run和docker ps、但还没认真研究过“镜像分发”的读者。
1. 为什么仓库是容器实践绕不开的一环
1.1 从镜像分层说起,仓库到底存了什么
很多初学者会有一个误区,觉得 Docker 镜像就是“一个文件”,仓库就是“放文件的网盘”。实际上镜像背后是一套 OCI 规范的东西:一个镜像由多个 layer 组成,每个 layer 在仓库里是以 blob 存的,另外还有 manifest 记录这些 layer 的清单和配置,更上层还有 tag 来引用不同的 manifest。
所以你在本地敲docker pull nginx:1.23的时候,Docker 客户端不是直接下“一个 nginx 文件”,而是先去拿 manifest,再根据 manifest 去上面列出的每个 layer 地址逐个下载。这也就是为什么仓库不是简简单单的 FTP——它必须有一套完整的 API 来回答问题:“这个 tag 对应的 manifest 是什么”“我该去哪层下文件”“这个 blob 存在吗”。所有 Docker 仓库,不管是 Hub、Registry 还是 Harbor,走的都是同一套/v2/开头的 Registry API,理解了这一点,你会发现三种方案的很多操作习惯其实是一样的。
仓库存在的价值也不仅仅是“存”。它是镜像分发的中间枢纽,是团队协作的共享基础,也是生产部署的安全边界。你没仓库的时候,一个镜像要从一台机器到另一台机器,要么docker save成 tar 再传过去docker load,要么推到某个临时地方。小规模还能忍,环境一多、镜像一多,这种“人肉分发”完全不可持续。
1.2 三种仓库方案的定位对照
既然仓库这么必要,市面上可选的就三件事:用公有的、自建轻量的、自建企业级的。它们不是互相替代的关系,而是平行适用不同场景。我见过很多团队一上来就折腾 Harbor,结果配置项看得头晕;也见过团队用 Registry 用了两三年,最后连个权限控制都没有。所以在动手之前,先想清楚自己的需求在哪一档。
| 方案 | 定位 | 部署成本 | 功能强度 | 适合场景 |
|---|---|---|---|---|
| Docker Hub | 公有 SaaS 仓库 | 零部署,注册即可用 | 拉取、推送、Web 管理、限流明确 | 个人开发、学习、公开镜像分发 |
| Registry | 官方轻量私有仓库 | 一个容器即可跑起来 | 基础拉取/推送、API 完整 | 内网小团队、边缘节点、测试环境 |
| Harbor | 企业级私有仓库 | 依赖 Docker Compose,组件较多 | 权限、项目、复制、扫描、审计、配额等 | 团队协作、生产交付、合规要求高的环境 |
选型的核心逻辑其实就一句话:你要不要权限管理,要不要团队协作流程。只要一个人用、机器数量少,Registry 足够;只要涉及多人、多项目、要能追溯到谁推了什么,直接用 Harbor。Docker Hub 更像默认选项,它的最大价值是“开箱即用”,但拉取限流和内网访问问题往往会逼着你往自建方向走。
2. Docker Hub:最省事的公有仓库,先把镜像命名和拉取规则搞懂
2.1 镜像命名规则与推送姿势
Docker Hub 用得再频繁,也先要把镜像命名规则说清楚。一个完整的镜像 tag 通常长这样:docker.io/library/nginx:1.23。拆开看,docker.io是 registry 地址,library是命名空间,nginx是仓库名,1.23是 tag。平时你敲nginx:1.23,Docker 会默认补全docker.io/library/前缀,这是最长被人忽略的细节。
如果你要推送自己的镜像到 Docker Hub,首先得先在 Hub 上注册账号并创建一个仓库。比如账号叫zhangsan,仓库名myapp,那本地镜像先要打标签:
docker tag myapp:v1.0 zhangsan/myapp:v1.0 docker login docker push zhangsan/myapp:v1.0docker login会提示输入用户名和密码,凭证会存在~/.docker/config.json里。注意,如果你在 CI 机器上不想交互式登录,可以用echo 'password' | docker login -u zhangsan --password-stdin这种方式。另外推送之前记得检查 tag 名称里有没有带docker.io/zhangsan/myapp,带上了也没关系,push时 Docker 会自动解析。
Hub 上有个很坑的限制是匿名拉取限流:未登录状态下每 6 小时只能拉 100 次,登录后是 200 次。听起来不少,但你在公司 NAT 后面,几十台机器共享同一个出口 IP,一天的 CI 构建很可能就把配额刷爆,报错往往是You have reached your pull rate limit。所以生产环境我从不建议依赖公共 Hub,至少应该登录状态下拉一次缓存到私有仓库。
2.2 拉取超时与网络不可达的常见处理
国内环境下,直接访问 Docker Hub 经常出现各种网络问题。最典型的就是拉镜像时报错:
docker: error during connect: Post "https://registry-1.docker.io/v2/": dial tcp: lookup registry-1.docker.io: no such host 或 could not reach the hub ([errno 101] network is unreachable). using cached c...errno 101在 Linux 里对应的就是网络不可达(ENETUNREACH),说明你的机器根本无法访问 Hub 的地址。这个问题的本质不是 Docker 配置导致的,而是主机的路由、DNS 或者防火墙层面压根过不去。排查顺序建议是:先ping registry-1.docker.io看域名能不能解析、ICMP 通不通,再看curl -I https://registry-1.docker.io/v2/能不能拿到响应。如果 DNS 解析失败,检查/etc/resolv.conf;如果 ping 不通,检查默认路由和出口防火墙规则。没有网络回程,在 Docker 端做多少配置都白搭。
在能上网但不稳定、或访问较慢的场景下,可以给 Docker daemon 配置镜像加速。编辑/etc/docker/daemon.json:
{ "registry-mirrors": ["https://your-mirror.example.com"] }之后重启systemctl restart docker。需要注意,registry-mirrors 是让 Docker 在拉公开镜像时去镜像站拉取,但你主动docker pull mydomain/myimage这种私有地址不会走 mirror,必须配合后面的私有仓库方案。另外,不建议在daemon.json里同时配置多个无关的registry-mirrors,因为 Docker 只会挑第一个可用的,配多了反而容易让人误解。
3. 自建轻量私有仓库:Registry 三分钟跑起来
3.1 一个命令启动基础版仓库
官方提供的 Registry 镜像是registry:2,它实现的就是一套最纯粹的 /v2/ API,无 Web 界面、无权限、无项目隔离。但这恰恰是它的优点:轻、稳、资源占用极低。小团队在内网做基础镜像分发,跑一个容器就够了。
启动一个带数据持久化的 Registry:
docker run -d --name registry --restart=always \ -p 5000:5000 \ -v /opt/registry-data:/var/lib/registry \ registry:2这里-v挂载目录必须做,否则容器一删,镜像全丢。Registry 把层数据都放在/var/lib/registry/docker/registry/v2下,我见过有人忘了挂载磁盘,容器销毁后数据全没,所以挂载这块要当成硬性要求。
起来之后怎么验证?直接在浏览器或 curl 请求它的 API:
curl http://127.0.0.1:5000/v2/_catalog正常会返回一个 JSON,里面是仓库列表。刚开始是{"repositories":[]},因为还没推过东西。到这一步,一个最简私有仓库已经能用了,我们把本地一个镜像推上去试试。
docker tag nginx:1.23 127.0.0.1:5000/nginx:1.23 docker push 127.0.0.1:5000/nginx:1.23如果你使用的还是默认的 Docker 配置,这条 push 大概率会报错:
http: server gave HTTP response to HTTPS client原因后面说,这也引出了私有仓库配置里最重要的一环。
3.2 让 Docker 客户端信任私有仓库
上面那个报错很经典:Registry 默认跑的是 HTTP,而 Docker 客户端默认只跟 HTTPS 仓库通信。解决方式有两个,一是给 Registry 配 TLS 证书走 HTTPS,二是在客户端声明“这个地址我接受 HTTP”。实验环境或者内网无敏感数据的场景,最推荐的是第二种。
在每台需要访问这个私有仓库的机器上,编辑/etc/docker/daemon.json:
{ "insecure-registries": ["192.168.1.10:5000"] }这里有两个容易被忽略的细节。第一,insecure-registries里的地址必须和镜像 tag 里的 registry 地址完全一致,包括端口。你写192.168.1.10但 tag 里用192.168.1.10:5000,照样不认。第二,改完 daemon.json 后要重启 Docker 服务,而且重启前最好先验证 JSON 语法:python3 -m json.tool /etc/docker/daemon.json。一旦 JSON 格式损坏,Docker 守护进程可能直接启动失败,那个时候连docker ps都敲不动。
配置完成后,重新执行:
docker tag nginx:1.23 192.168.1.10:5000/nginx:1.23 docker push 192.168.1.10:5000/nginx:1.23 docker pull 192.168.1.10:5000/nginx:1.23在另一台机器上也能拉取时,你的内网镜像分发链路就通了。注意,跨机器拉取时同样要在目标机器上配置insecure-registries,否则那边也会报 HTTPS 的错。
3.3 加一层认证:htpasswd 让仓库不是裸奔
裸奔的 Registry 只要知道地址,任何人都能往里推镜像。稍微正式一点的环境,至少要加一层 HTTP Basic Auth。做法很简单,用htpasswd生成一个口令文件。
用官方 httpd 镜像一行命令生成:
docker run --rm --entrypoint htpasswd httpd:2 -Bbn zhangsan 'yourpassword' > auth/htpasswd注意-B表示 bcrypt 加密算法,比默认的 MD5 安全得多。之后重新启动 Registry,把认证文件挂载进去,并设置环境变量:
docker run -d --name registry --restart=always \ -p 5000:5000 \ -v /opt/registry-data:/var/lib/registry \ -v /opt/registry-auth:/auth \ -e "REGISTRY_AUTH=htpasswd" \ -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \ -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \ registry:2启动后未登录推送:
docker push 192.168.1.10:5000/nginx:1.23会收到类似no basic auth credentials或401 Unauthorized的错误。先执行docker login 192.168.1.10:5000输入账号密码,再推送就正常了。这就是 Registry 能做的最简认证方案。它没有用户注册流程,没有 Web 管理界面,账号要靠管理员手工生成,所以从这一层开始,你已经偏向 Harbor 的场景了。
4. 企业级私有仓库 Harbor:从下载、配置到跑起来的完整过程
4.1 Harbor 到底比 Registry 多做了哪些事
Harbor 最早由 VMware 开源,后来进了 CNCF 基金会,是目前企业自建镜像仓库事实上的主流选择。它不是重新造了一个 registry,而是基于 registry 做了一层很厚的封装:外面套了 nginx 做流量入口和 TLS 终止,core 组件做鉴权和项目管理,数据库存用户和权限,jobservice 做复制和清理任务,portal 提供 Web 界面,甚至还能挂 trivy 做漏洞扫描。组件多,功能自然也多。
最核心的几个能力我用自己的话说:
- 项目级隔离:仓库从扁平结构变成项目空间,不同团队各管各的,互不可见。
- 细粒度权限:项目内有 Guest、Developer、Maintainer、ProjectAdmin 等角色,能控制谁能拉、谁能推。
- 镜像复制:配置一个复制规则,把内网镜像自动同步到另一个 Harbor 或外部仓库。
- 配额管理:限制一个项目最多占多少磁盘,防止有人把仓库当网盘。
- 回收站和 GC:删除镜像后不会立刻释放磁盘,需要手动执行垃圾回收,这个设计让不少第一次用的人踩坑。
如果你只需要“能在网页上看看镜像、控制谁能推”,Harbor 就是为这个场景准备的。但代价是部署组件多,依赖 Docker Compose,对主机的 CPU 和内存有基础要求,一般建议至少 2C4G 的机器。
4.2 离线包安装 Harbor 的完整操作
Harbor 官方提供在线安装包和离线安装包。离线包里内置了所有依赖镜像的 tar 包,安装时不需要大量拉取镜像,在公网出口受限的环境下强烈建议选 offline 版本。下载后解压:
tar zxf harbor-offline-installer-v2.11.0.tgz cd harbor目录下会有一个harbor.yml.tmpl模板,先复制成harbor.yml:
cp harbor.yml.tmpl harbor.yml然后改配置。最关键的几个字段:
hostname: 192.168.1.10 http: port: 80 harbor_admin_password: YourStrongPassword123 database: password: YourDBPassword123 data_volume: /datahostname是你访问 Harbor 的地址,可以是 IP 或域名,但绝对不能写http://前缀,这是配置校验失败的重灾区。harbor_admin_password是管理员初始密码,长度要足够且包含大小写。data_volume尽量指到一个容量够大的独立磁盘。
完成配置后,执行./install.sh。脚本会自动检查 docker 和 docker compose 环境,然后逐个启动组件。如果没装 docker compose 插件,Harbor 会直接报错提示,先执行:
sudo apt install docker-compose-plugin # 或者使用 docker compose v2,确认 docker compose version 有输出安装过程最后会输出✔ ----Harbor has been installed and started successfully----。这时候用docker compose ps看容器状态:
cd /path/to/harbor && docker compose ps你会看到harbor-core、harbor-db、harbor-jobservice、harbor-portal、harbor-registry、nginx、redis等多个容器都在 Up 状态。浏览器访问http://192.168.1.10,默认 80 端口,用admin加刚才设置的密码登录,Harbor 的 Web 界面就出来了。
4.3 把镜像推送到 Harbor 并接入日常开发
Harbor 启动后,首件事是在 Web 界面创建一个项目,比如叫demo,访问级别可以先设为私有。然后在客户端机器上配置insecure-registries,把 Harbor 地址加进去并重启 Docker。这里要注意,Harbor 通过 nginx 暴露服务,默认 80 端口,所以地址要写成192.168.1.10或192.168.1.10:80,取决于你的 daemon.json 写法,保持一致就行。
登录并推送:
docker login 192.168.1.10 docker tag myapp:v1.0 192.168.1.10/demo/myapp:v1.0 docker push 192.168.1.10/demo/myapp:v1.0看到digest:字样的返回就说明推成功了。此时在 Web 界面进入demo项目,能看到镜像列表和对应的 tag。之后任何机器只要docker login成功,就能拉取这个项目下的镜像。
日常接入 CI 时,更推荐用 Harbor 的“机器人账户”而不是真实账号。在项目的“机器人账户”页面新建一个,系统会给一个 username 和一个以dockerconfigjson开头的 token。用法和普通登录一样:
docker login -u 'robot$demo+ci' -p 'the-token-value' 192.168.1.10机器人账户的权限可以限制到单个项目,即使泄漏,影响面也可控。这一点在团队协作里非常实用。
5. 常见问题排查实录
5.1 关键词速查表
这些年在仓库问题上踩坑踩得实在不少,我把最常见的问题按“报错关键词”整理成一张速查表,方便按图索骥。
| 报错/现象 | 可能原因 | 处理方式 |
|---|---|---|
network is unreachable / errno 101 | 宿主机网络不通、DNS 解析失败 | 先排查路由与 DNS,再配置 mirror 或走内网仓库 |
http: server gave HTTP response to HTTPS client | 仓库是 HTTP,客户端默认走 HTTPS | 在 daemon.json 的 insecure-registries 增加对应地址并重启 |
x509: certificate signed by unknown authority | 自签名证书未被信任 | 拷贝 CA 到/etc/docker/certs.d/或改用 insecure-registries |
denied: requested access to the resource is denied | 未登录、无权限或项目不存在 | 检查账号权限、镜像 tag 里的项目名是否正确 |
manifest unknown | tag 不存在、推送未完成、仓库被 GC 清理 | 确认 tag 名称和推送到正确的仓库地址 |
no space left on device | 数据盘或/var/lib/docker满了 | 清理无用镜像、扩大数据盘 |
harbor failed with config validation | harbor.yml 配置格式或内容不合规 | 重点检查 hostname 是否含协议前缀、端口冲突、密码过短、YAML 缩进 |
413 Request Entity Too Large | nginx 默认上传大小限制 | 调整 Harbor 内置 nginx 的 client_max_body_size 配置并重启 |
这张表里出现频率最高的其实还是前三条。很多新手一看到 HTTPS 相关报错就以为是证书问题,拼命配证书,结果忘了检查仓库到底是不是 HTTP;一看到 network unreachable 就怀疑 Docker 有毛病,结果发现是机器网关断了。排查顺序建议永远是:网络层优先,仓库配置其次,Docker 配置最后。
5.2 三个容易让人栽跟头的深层细节
第一个坑是修改daemon.json导致的 Docker 启动失败。很多人拿 Linux 那套思维,直接vim /etc/docker/daemon.json然后一顿改,结果多打了个逗号,重启 Docker 后服务起不来,连docker ps都报cannot connect to the Docker daemon。这时候补救方法是把daemon.json先备份,然后删除或改回{},用journalctl -u docker看具体报错,修复后再重启。我建议所有改 daemon.json 前,先跑一遍python3 -m json.tool /etc/docker/daemon.json验证格式,这行命令不值钱,但能救你一个下午。
第二个坑是 Harbor 配置校验失败。报错提示harbor happened in config validation,这其实是 Harbor 在./install.sh执行时对harbor.yml做检查。最常见的死法就是把hostname写成hostname: http://192.168.1.10,Harbor 要求的是纯主机名或 IP,不带你协议名;另一个死法是https段没有启用却保留了证书文件路径,或者证书文件根本不存在;还有一种是端口 80 或 443 已经被别的进程占用。遇到这个报错不要慌,./install.sh会明确提示是哪一项校验失败,仔细读输出,基本都指向修改harbor.yml并重新执行。
第三个坑是动不动就“删除镜像,但磁盘没释放”。Harbor 的 UI 里删除一个 tag,逻辑上只是把 manifest 标记为可回收,blob 层还在磁盘上,只有执行“垃圾回收”后才能真正释放空间。如果你发现删了一堆镜像,df -h却一点没变,这是正常现象。Harbor 的垃圾回收路径在 Web 界面的“系统管理”里,执行 GC 时最好选在没有构建任务的时段,因为 GC 过程中 registry 的写入会受影响。Registry 裸仓库更明显,需要手动进入容器执行:
docker exec registry /bin/registry garbage-collect /etc/docker/registry/config.yml --delete-untagged并且 GC 之后,仓库 API 里的_catalog列表可能还会显示残留的 repo,通常要再删掉_manifests/repositories目录或做二次清理,这是 Registry 设计上一个让人难受的地方,但了解它就不至于傻等磁盘自动变干净。
5.3 一些日常运维上的实用经验
最后分享几个我自己的实践习惯,不一定写在哪本官方文档里,但确实省过不少事。
第一个是镜像 tag 规范。我建议 tag 不要用latest,要带版本号,最好是流水号或 Git commit。latest的最大问题是不可追踪:你今天 pull 和明天 pull,拿到的可能就不是同一个东西。生产环境里我把 tag 直接打成项目名-commit号,比如order-service-7f3a2c1,出了问题能精确定位是哪一版代码构建出来的。
第二个是“推镜像前先看磁盘”。Registry 和 Harbor 的垃圾回收机制都不同于普通文件系统,删除不是立即释放,所以部署时宁可把数据盘规划大一点,也不要等到报no space left再临时清理。团队多了以后,配额功能一定要开,Harbor 项目里设个 10GB 或者 20GB 的限额,能有效防止有人堆一堆没用的镜像。
第三个是对外访问不稳定的环境,尽量把依赖外网的拉取前置。比如在 CI 构建机配置里把docker build的--pull参数去掉,或者确保构建基础镜像已经预拉到本地,不然构建高峰期网络一抖,整个流水线跟着崩,那种“using cached ...”或者network unreachable的报错会把你折腾得没脾气。
从最小的 Registry 到完整的 Harbor 其实是一条很平滑的演进路线:自己一个人用,Registry 带上认证就能撑很久;团队开始正规协作,Harbor 的权限和项目隔离就是刚需;Docker Hub 则是始终存在的默认选项,适合不需要私有化的公开镜像。我自己的体会是,先把 Registry 跑通,理解 tag、推送、认证这些基本链路,再迁移到 Harbor,比一上来就部署全家桶要顺手得多。镜像分发这件事最难的从来不是命令,而是一开始就能想清楚“谁在推、谁在拉、怎么控权、怎么放数据”,把这几个问题想明白,仓库就不会成为你容器路上的瓶颈。