概述
镜像做好了总得有地方放。本文把私有镜像仓库从搭建到推拉镜像的整条链路跑通,重点说三个高频坑:tag 命名写错把镜像推到 Docker Hub、http 私服被 Docker 拒绝、daemon.json 改错导致 Docker 起不来。
纲要
- 镜像仓库(Docker Registry)
- 公共仓库:Docker Hub、阿里云容器镜像服务、网易云等
- 私有仓库:企业自建,
registry官方镜像 + 可选图形界面
- 简化版私有仓库
docker run一条命令,默认端口 5000,数据卷挂在/var/lib/registry
- 信任地址配置
/etc/docker/daemon.json加insecure-registries- 改完必须
systemctl daemon-reload+systemctl restart docker
- 镜像命名与推拉
- 命名规则:
仓库地址/镜像名:版本 docker tag→docker push→docker pull三步及预期输出
- 命名规则:
- 图形界面版本
docker-compose.yml组合部署registry+joxit/docker-registry-ui
- 排查
- tag 写错 / http 被拒 / daemon.json 写挂的回滚办法
镜像仓库是什么
Registry 直译叫"注册中心",是把镜像注册上去的服务;叫镜像仓库更顺口,指的都是同一个东西。前面用到的 docker pull redis、docker pull mysql 走的都是 Docker Hub------公共镜像仓库。
问题在于:企业里基于微服务业务构建的镜像,里面是业务代码和公司配置,属于商业机密。推到公共仓库,等于源码公开。所以自建私有仓库是刚需,不是可选项。
#mermaid-svg-DSkk2IlHK1EM9w0N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DSkk2IlHK1EM9w0N .error-icon{fill:#552222;}#mermaid-svg-DSkk2IlHK1EM9w0N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DSkk2IlHK1EM9w0N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DSkk2IlHK1EM9w0N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DSkk2IlHK1EM9w0N .marker.cross{stroke:#333333;}#mermaid-svg-DSkk2IlHK1EM9w0N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DSkk2IlHK1EM9w0N p{margin:0;}#mermaid-svg-DSkk2IlHK1EM9w0N .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DSkk2IlHK1EM9w0N .cluster-label text{fill:#333;}#mermaid-svg-DSkk2IlHK1EM9w0N .cluster-label span{color:#333;}#mermaid-svg-DSkk2IlHK1EM9w0N .cluster-label span p{background-color:transparent;}#mermaid-svg-DSkk2IlHK1EM9w0N .label text,#mermaid-svg-DSkk2IlHK1EM9w0N span{fill:#333;color:#333;}#mermaid-svg-DSkk2IlHK1EM9w0N .node rect,#mermaid-svg-DSkk2IlHK1EM9w0N .node circle,#mermaid-svg-DSkk2IlHK1EM9w0N .node ellipse,#mermaid-svg-DSkk2IlHK1EM9w0N .node polygon,#mermaid-svg-DSkk2IlHK1EM9w0N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DSkk2IlHK1EM9w0N .rough-node .label text,#mermaid-svg-DSkk2IlHK1EM9w0N .node .label text,#mermaid-svg-DSkk2IlHK1EM9w0N .image-shape .label,#mermaid-svg-DSkk2IlHK1EM9w0N .icon-shape .label{text-anchor:middle;}#mermaid-svg-DSkk2IlHK1EM9w0N .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DSkk2IlHK1EM9w0N .rough-node .label,#mermaid-svg-DSkk2IlHK1EM9w0N .node .label,#mermaid-svg-DSkk2IlHK1EM9w0N .image-shape .label,#mermaid-svg-DSkk2IlHK1EM9w0N .icon-shape .label{text-align:center;}#mermaid-svg-DSkk2IlHK1EM9w0N .node.clickable{cursor:pointer;}#mermaid-svg-DSkk2IlHK1EM9w0N .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DSkk2IlHK1EM9w0N .arrowheadPath{fill:#333333;}#mermaid-svg-DSkk2IlHK1EM9w0N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DSkk2IlHK1EM9w0N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DSkk2IlHK1EM9w0N .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DSkk2IlHK1EM9w0N .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DSkk2IlHK1EM9w0N .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DSkk2IlHK1EM9w0N .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DSkk2IlHK1EM9w0N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DSkk2IlHK1EM9w0N .cluster text{fill:#333;}#mermaid-svg-DSkk2IlHK1EM9w0N .cluster span{color:#333;}#mermaid-svg-DSkk2IlHK1EM9w0N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DSkk2IlHK1EM9w0N .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DSkk2IlHK1EM9w0N rect.text{fill:none;stroke-width:0;}#mermaid-svg-DSkk2IlHK1EM9w0N .icon-shape,#mermaid-svg-DSkk2IlHK1EM9w0N .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DSkk2IlHK1EM9w0N .icon-shape p,#mermaid-svg-DSkk2IlHK1EM9w0N .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DSkk2IlHK1EM9w0N .icon-shape .label rect,#mermaid-svg-DSkk2IlHK1EM9w0N .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DSkk2IlHK1EM9w0N .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DSkk2IlHK1EM9w0N .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DSkk2IlHK1EM9w0N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} docker pull 基础镜像
docker push
docker pull
docker pull
docker pull
Docker Hub
公共仓库
开发机
docker build
私有 Registry
192.168.150.101:8080
应用服务器 1
应用服务器 2
测试环境
公共仓库与私有仓库怎么选
| 维度 | 公共仓库(Docker Hub) | 公共仓库(阿里云容器镜像服务) | 自建私有仓库 |
|---|---|---|---|
| 网络 | 境外,国内拉取经常超时 | 国内节点,速度快 | 内网,最快,不受公网波动影响 |
| 私有镜像 | 免费额度仅 1 个私有仓库 | 有免费私有配额 | 无限制,自己说了算 |
| 安全性 | 镜像出境,合规风险高 | 国内合规,但数据仍在他方 | 数据只在自己机房 |
| 搭建成本 | 0 | 0 | 一条 docker run 或一份 compose 文件 |
| 运维成本 | 0 | 0 | 要管磁盘、备份、清理 |
| 图形界面 | 有 | 有 | 简化版没有,需额外部署 UI |
| 适用 | 开源基础镜像、个人项目 | 中小企业、无专职运维 | 有合规要求 / 内网隔离 / 离线环境 |
实际项目里通常是混合用法:基础镜像(JDK、Nginx、Redis)从公共仓库拉加个加速器,业务镜像全部进私有仓库。
搭建私有仓库基于 Docker 官方提供的 registry 镜像,官网地址。它有两种玩法。
简化版私有仓库
官方的 Docker Registry 是基础版本,具备仓库管理的完整功能,只是没有图形化界面。一条命令就能跑起来:
bash
docker run -d \
--restart=always \
--name registry \
-p 5000:5000 \
-v registry-data:/var/lib/registry \
registry
几个参数都得说清楚:
-p 5000:5000:registry 服务默认监听 5000 端口,这个是固定的-v registry-data:/var/lib/registry:/var/lib/registry是仓库存镜像数据的目录,挂成数据卷,容器删了数据还在--restart=always:机器重启后仓库自己爬起来,否则每次开机都要手动拉
跑起来之后先看一眼有没有起来:
bash
docker ps
docker logs -f registry
仓库里现在有什么镜像,用 HTTP API 查:
bash
curl http://192.168.150.101:5000/v2/_catalog
text
{"repositories":[]}
空仓库返回空数组,说明服务正常。注意这里用的是 http,不是 https------这正是后面要配信任地址的原因。
目录结构(数据卷内容):
tree
/var/lib/docker/volumes/registry-data/_data/
└── docker/
└── registry/
└── v2/
├── blobs/ # 镜像层数据,按 sha256 摘要分片存储
│ └── sha256/
└── repositories/ # 仓库与标签的索引
└── nginx/
├── _layers/
├── _manifests/
│ ├── revisions/
│ └── tags/
└── _uploads/ # 上传中的临时文件
blobs 放实际的层数据,repositories 放名字和标签到摘要的映射。同一个镜像 push 两次,第二次会命中已有 blob 直接跳过------这就是分层存储在仓库侧的体现。
配置 Docker 信任地址
私服用的是 http 协议,Docker 默认只信任 https,直接 push 会被拒。所以要显式声明这个地址是"不安全但可信"的。
打开配置文件:
bash
vi /etc/docker/daemon.json
假设里面已经有一个阿里云镜像加速器,加上 insecure-registries 后完整内容如下:
json
{
"registry-mirrors": ["https://xxxxxx.mirror.aliyuncs.com"],
"insecure-registries": ["http://192.168.150.101:8080"]
}
三个细节:
- 两个配置项之间是逗号分隔。少一个逗号,JSON 非法,整个文件不生效甚至 Docker 起不来
- 地址要写自己的机器 IP,别照抄
192.168.150.101 - 端口跟着实际部署走:简化版 registry 是
5000,带 UI 的 compose 版本对外是8080
这里要交代清楚一个切换点:前文 docker run 起的简化版监听 5000,而后文推镜像、拉镜像全部用的是 192.168.150.101:8080------那是带 UI 的 compose 版(见后面「图形界面(docker-compose 方案)」一节)。也就是说,从这里开始我们换成 compose 方案了。切换动作就两条:
bash
docker rm -f registry
停掉简化版(数据卷 registry-data 还在,已推过的镜像不会丢),再用后文的 compose 文件把 registry + UI 起起来,对外就是 8080。如果不想折腾 UI、继续用简化版,那就把后文所有命令里的 8080 换成 5000,insecure-registries 也写成 5000,其余步骤完全一样。两边端口对不上,push 时就会撞 connection refused。
改完必须重新加载配置并重启 Docker,少了这步改动不会生效:
bash
systemctl daemon-reload
systemctl restart docker
验证配置有没有被读到:
bash
docker info | grep -A 5 "Insecure Registries"
text
Insecure Registries:
192.168.150.101:8080
127.0.0.0/8
有输出就说明配进去了。
镜像命名规则:最容易写错的地方
推送镜像到私有仓库前必须先 tag,也就是给镜像重新起个名字。原因是镜像名决定了 Docker 把它推到哪:
text
[仓库地址/][命名空间/]镜像名[:标签]
Docker 判断"仓库地址"这段的规则是:看第一段里有没有 . 或者 :,或者是 localhost。有,就当成仓库主机名;没有,就默认推 Docker Hub。
#mermaid-svg-6o0S3Hq0JFwwS0Cb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .error-icon{fill:#552222;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .marker.cross{stroke:#333333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb p{margin:0;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .cluster-label text{fill:#333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .cluster-label span{color:#333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .cluster-label span p{background-color:transparent;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .label text,#mermaid-svg-6o0S3Hq0JFwwS0Cb span{fill:#333;color:#333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .node rect,#mermaid-svg-6o0S3Hq0JFwwS0Cb .node circle,#mermaid-svg-6o0S3Hq0JFwwS0Cb .node ellipse,#mermaid-svg-6o0S3Hq0JFwwS0Cb .node polygon,#mermaid-svg-6o0S3Hq0JFwwS0Cb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .rough-node .label text,#mermaid-svg-6o0S3Hq0JFwwS0Cb .node .label text,#mermaid-svg-6o0S3Hq0JFwwS0Cb .image-shape .label,#mermaid-svg-6o0S3Hq0JFwwS0Cb .icon-shape .label{text-anchor:middle;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .rough-node .label,#mermaid-svg-6o0S3Hq0JFwwS0Cb .node .label,#mermaid-svg-6o0S3Hq0JFwwS0Cb .image-shape .label,#mermaid-svg-6o0S3Hq0JFwwS0Cb .icon-shape .label{text-align:center;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .node.clickable{cursor:pointer;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .arrowheadPath{fill:#333333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6o0S3Hq0JFwwS0Cb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6o0S3Hq0JFwwS0Cb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6o0S3Hq0JFwwS0Cb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .cluster text{fill:#333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .cluster span{color:#333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6o0S3Hq0JFwwS0Cb rect.text{fill:none;stroke-width:0;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .icon-shape,#mermaid-svg-6o0S3Hq0JFwwS0Cb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .icon-shape p,#mermaid-svg-6o0S3Hq0JFwwS0Cb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .icon-shape .label rect,#mermaid-svg-6o0S3Hq0JFwwS0Cb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6o0S3Hq0JFwwS0Cb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6o0S3Hq0JFwwS0Cb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6o0S3Hq0JFwwS0Cb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
docker push nginx:1.0
镜像名第一段
含 . 或 : 或 localhost ?
解析为 Docker Hub
实际推 docker.io/library/nginx:1.0
解析为指定仓库主机
该地址在 insecure-registries 中?
HTTP 通信,成功
强制 HTTPS
私服不支持则报 server gave HTTP response
未登录报 denied
已登录则真的推上公网
所以这两种写法天差地别:
| 镜像名 | 推到哪 |
|---|---|
nginx:1.0 |
Docker Hub 官方库(docker.io/library) |
zhangsan/nginx:1.0 |
Docker Hub 的 zhangsan 用户命名空间 |
192.168.150.101:8080/nginx:1.0 |
私有仓库(含端口,被识别为主机) |
registry.example.com/nginx:1.0 |
私有仓库(含点号,被识别为主机) |
localhost:5000/nginx:1.0 |
本机仓库(localhost 特判) |
docker tag 只是给同一个镜像加一个新引用,不会复制镜像数据。执行完两个名字的 IMAGE ID 完全一样:
bash
docker tag nginx:latest 192.168.150.101:8080/nginx:1.0
docker images
text
REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.150.101:8080/nginx 1.0 605c77e624dd 8 months ago 141MB
nginx latest 605c77e624dd 8 months ago 141MB
605c77e624dd 一模一样,就是同一个镜像挂了两个名字。
推送与拉取完整流程
推上去
bash
docker push 192.168.150.101:8080/nginx:1.0
text
The push refers to repository [192.168.150.101:8080/nginx]
d874fd2bc83b: Pushed
32ce5f6a5106: Pushed
f1db227348d0: Pushed
b8d6e692a25e: Pushed
9eb82f04c782: Mounted from library/nginx
1.0: digest: sha256:2e0f1b0d6d2a4e0e6f8d1f2a1e7c5e1a4b6b4c0d8f3a2b1c0d9e8f7a6b5c4d3e size: 1570
最后那行 digest 是镜像清单的 sha256 摘要,pull 时可以用它精确指定版本,比 tag 可靠------tag 可以被覆盖,摘要不能。
拉下来
先把本地的删干净,验证是真的从仓库拉的:
bash
docker rmi 192.168.150.101:8080/nginx:1.0
docker rmi nginx:latest
docker images | grep nginx
此时应该没有任何 nginx 镜像。然后拉:
bash
docker pull 192.168.150.101:8080/nginx:1.0
text
1.0: Pulling from nginx
Digest: sha256:2e0f1b0d6d2a4e0e6f8d1f2a1e7c5e1a4b6b4c0d8f3a2b1c0d9e8f7a6b5c4d3e
Status: Downloaded newer image for 192.168.150.101:8080/nginx:1.0
192.168.150.101:8080/nginx:1.0
推拉的数据流
私有 Registry Docker Daemon Docker Client 私有 Registry Docker Daemon Docker Client #mermaid-svg-Z96YIBZiPbx9lKT3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z96YIBZiPbx9lKT3 .error-icon{fill:#552222;}#mermaid-svg-Z96YIBZiPbx9lKT3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z96YIBZiPbx9lKT3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z96YIBZiPbx9lKT3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z96YIBZiPbx9lKT3 .marker.cross{stroke:#333333;}#mermaid-svg-Z96YIBZiPbx9lKT3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z96YIBZiPbx9lKT3 p{margin:0;}#mermaid-svg-Z96YIBZiPbx9lKT3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z96YIBZiPbx9lKT3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Z96YIBZiPbx9lKT3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Z96YIBZiPbx9lKT3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Z96YIBZiPbx9lKT3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Z96YIBZiPbx9lKT3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Z96YIBZiPbx9lKT3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Z96YIBZiPbx9lKT3 .sequenceNumber{fill:white;}#mermaid-svg-Z96YIBZiPbx9lKT3 #sequencenumber{fill:#333;}#mermaid-svg-Z96YIBZiPbx9lKT3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Z96YIBZiPbx9lKT3 .messageText{fill:#333;stroke:none;}#mermaid-svg-Z96YIBZiPbx9lKT3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z96YIBZiPbx9lKT3 .labelText,#mermaid-svg-Z96YIBZiPbx9lKT3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Z96YIBZiPbx9lKT3 .loopText,#mermaid-svg-Z96YIBZiPbx9lKT3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Z96YIBZiPbx9lKT3 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Z96YIBZiPbx9lKT3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Z96YIBZiPbx9lKT3 .noteText,#mermaid-svg-Z96YIBZiPbx9lKT3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Z96YIBZiPbx9lKT3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z96YIBZiPbx9lKT3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z96YIBZiPbx9lKT3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Z96YIBZiPbx9lKT3 .actorPopupMenu{position:absolute;}#mermaid-svg-Z96YIBZiPbx9lKT3 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Z96YIBZiPbx9lKT3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Z96YIBZiPbx9lKT3 .actor-man circle,#mermaid-svg-Z96YIBZiPbx9lKT3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Z96YIBZiPbx9lKT3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} docker tag nginx:latest 192.168.150.101:8080/nginx:1.0 新增引用,镜像 ID 不变 docker push 192.168.150.101:8080/nginx:1.0 GET /v2/ 探测协议与认证方式 200 OK HEAD /v2/nginx/blobs/sha256:xxx 探测层是否已存在 404 不存在,需要上传 POST /v2/nginx/blobs/uploads/ 开启上传会话 202 Accepted,返回上传地址 PATCH/PUT 逐层上传 blob PUT /v2/nginx/manifests/1.0 提交清单 201 Created 1.0: digest: sha256:xxx size: 1570 docker pull 192.168.150.101:8080/nginx:1.0 GET /v2/nginx/manifests/1.0 取清单 返回层摘要列表 GET /v2/nginx/blobs/sha256:xxx 逐层下载 层数据 Status: Downloaded newer image
注意 push 时的 HEAD blobs 那一步:已经存在的层会直接跳过(日志里显示 Mounted from 或 Layer already exists),只传差异部分。这也是为什么第二次推同一个镜像会快很多。
pull 下来的镜像名字是 192.168.150.101:8080/nginx:1.0,很长。想用回短名字就再 tag 一次:
bash
docker tag 192.168.150.101:8080/nginx:1.0 nginx:1.0
带图形界面的版本
简化版功能齐全,但查镜像只能敲 curl 看 JSON,不方便。图形界面不是 Docker 官方做的,是第三方在官方 registry 基础上额外开发的服务,所以要两个容器一起跑------用 docker-compose 组合部署最省事。
先建个专属目录,别把 compose 文件直接扔在根目录:
bash
mkdir -p /root/registry-ui
cd /root/registry-ui
目录结构:
tree
registry-ui/
├── docker-compose.yml # 编排文件,registry + ui 两个服务
└── registry-data/ # 启动后自动生成,bind mount 到容器内
└── docker/
└── registry/
└── v2/
├── blobs/
└── repositories/
docker-compose.yml 内容:
yaml
version: '3.0'
services:
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui:
image: joxit/docker-registry-ui:static
ports:
- 8080:80
environment:
- REGISTRY_TITLE=传智教育私有仓库
- REGISTRY_URL=http://registry:5000
depends_on:
- registry
几个配置点的含义:
registry服务没有 ports 映射,5000 端口只在 compose 内部网络可见,宿主机访问不到。这是有意的------仓库只对内暴露,外面只走 UI 那一个口子REGISTRY_URL=http://registry:5000里的registry是服务名。docker-compose 里多个服务之间用服务名互相访问,Docker 内置 DNS 会把它解析成容器 IPREGISTRY_TITLE只是网页标题,随便写depends_on保证先起 registry 再起 UI,避免 UI 启动时连不上仓库
启动:
bash
docker-compose up -d
text
Creating network "registry-ui_default" with the default driver
Creating registry-ui_registry_1 ... done
Creating registry-ui_ui_1 ... done
看日志确认启动状态:
bash
docker-compose logs -f
浏览器访问 http://192.168.150.101:8080:
doc
+------------------------------------------------------------+
| 传智教育私有仓库 |
+------------------------------------------------------------+
| [ 搜索镜像 ... ] [Go] |
+------------------------------------------------------------+
| |
| nginx 1 images |
| +----------------------------------------------------+ |
| | 1.0 | |
| | 大小 141 MB 创建时间 2022-02-28 10:23 | |
| | [ 复制 pull 命令 ] | |
| +----------------------------------------------------+ |
| |
+------------------------------------------------------------+
| 总计 1 images |
+------------------------------------------------------------+
要点:
- 顶部标题取自
REGISTRY_TITLE,可自定义为公司名或环境名(如"生产环境私有仓库") - 列表按仓库名聚合,标签折叠在下面;数字表示标签数量
- 点标签那行会展开详细信息,并把
docker pull 192.168.150.101:8080/nginx:1.0完整命令复制到剪贴板,省得手敲 - 初始状态显示
0 images,push 之后刷新页面才会更新
UI 容器内置 nginx,会把 /v2/ 开头的 API 请求转发到 REGISTRY_URL 指向的 registry,所以对外只需要暴露 8080 一个端口,push 和 pull 也都走 8080。如果只部署简化版 registry(没有 UI),就用 5000 ,insecure-registries 和 push 地址都要跟着改成 5000。
实战中的坑
tag 写错,镜像推到 Docker Hub
bash
# 错误:少了仓库地址前缀
docker tag nginx:latest nginx:1.0
docker push nginx:1.0
text
The push refers to repository [docker.io/library/nginx]
...
denied: requested access to the resource is denied
未登录会报 denied,看起来"失败了",但如果这台机器之前 docker login 过 Docker Hub 且账号有 library/nginx 权限,镜像就真被推到公网了。业务镜像泄露就是这么发生的。
排查办法:push 输出第一行会明确告诉你推到哪:
text
The push refers to repository [192.168.150.101:8080/nginx] # 对
The push refers to repository [docker.io/library/nginx] # 错
看到 docker.io 立刻 Ctrl+C。
http 私服被拒
没配 insecure-registries 就 push,Docker 会用 https 去连私服:
text
The push refers to repository [192.168.150.101:8080/nginx]
Get "https://192.168.150.101:8080/v2/": http: server gave HTTP response to HTTPS client
这个报错信息很直白:服务端给的是 HTTP 响应,客户端却在等 HTTPS。回去配 insecure-registries,别忘了重启 Docker。
反过来还有一种情况:insecure-registries 里写了 192.168.150.101:8080,但 push 时写的是 192.168.150.101:5000,端口对不上,信任配置不生效,同样报这个错。地址和端口必须完全一致。
daemon.json 写挂,Docker 起不来
多一个逗号、少一个逗号、用了中文引号,Docker 都起不来:
text
Job for docker.service failed because the control process exited with error code.
See "systemctl status docker.service" and "journalctl -xe" for details.
改之前先备份,这是唯一可靠的保险:
bash
cp /etc/docker/daemon.json /etc/docker/daemon.json.bak
已经挂了的话,按这个顺序处理:
bash
# 1. 看具体错误
systemctl status docker
journalctl -u docker -n 50 --no-pager
# 2. 校验 JSON 是否合法,会直接指出出错的行
python3 -m json.tool /etc/docker/daemon.json
# 3. 修不好就把文件移走,Docker 会用默认配置启动
mv /etc/docker/daemon.json /tmp/daemon.json.bad
systemctl start docker
# 4. 确认服务恢复后再从备份改回来
cp /etc/docker/daemon.json.bak /etc/docker/daemon.json
systemctl daemon-reload
systemctl restart docker
经验是先备份再改,别在已经起不来的状态下手改 JSON------越急越容易改出新错。
顺带一提的其他坑
| 现象 | 原因 | 处理 |
|---|---|---|
no basic auth credentials |
私服开了认证但没登录 | docker login 192.168.150.101:8080 |
connection refused |
仓库容器没起来或端口没映射 | docker ps 看容器状态,docker logs registry 看日志 |
unknown blob / push 中断 |
上传过程被中断,残留临时文件 | 删掉重推,或清理 /var/lib/registry/docker/registry/v2/repositories/<name>/_uploads |
| 磁盘被撑满 | 仓库不会自动清理旧镜像 | 定期做垃圾回收:docker exec -it registry bin/registry garbage-collect /etc/docker/registry/config.yml(前提:容器以 REGISTRY_STORAGE_DELETE_ENABLED=true 启动,且容器名叫 registry) |
| UI 页面空白或一直转圈 | REGISTRY_URL 写错,UI 连不上 registry |
必须是容器内的服务名 http://registry:5000,不是宿主机 IP |
API 速览
Registry HTTP API v2 常用端点:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /v2/ |
健康检查,返回 {} 表示服务可用 |
| GET | /v2/_catalog |
列出所有仓库,支持 ?n=100&last=xxx 分页 |
| GET | /v2/{name}/tags/list |
列出某个仓库的全部标签 |
| GET | /v2/{name}/manifests/{reference} |
取镜像清单,reference 可以是 tag 或 digest |
| HEAD | /v2/{name}/blobs/{digest} |
探测层是否存在,push 前用 |
| DELETE | /v2/{name}/manifests/{digest} |
删除镜像,需设置 REGISTRY_STORAGE_DELETE_ENABLED=true |
常用命令:
bash
# 列出仓库里所有镜像
curl http://192.168.150.101:8080/v2/_catalog
# 列出 nginx 的所有标签
curl http://192.168.150.101:8080/v2/nginx/tags/list
# 登录私有仓库
docker login 192.168.150.101:8080
# 重新打标签(前缀必须是仓库地址)
docker tag nginx:latest 192.168.150.101:8080/nginx:1.0
# 推送
docker push 192.168.150.101:8080/nginx:1.0
# 拉取
docker pull 192.168.150.101:8080/nginx:1.0
# 按 digest 拉取,版本不会被覆盖
docker pull 192.168.150.101:8080/nginx@sha256:2e0f1b0d6d2a4e0e6f8d1f2a1e7c5e1a4b6b4c0d8f3a2b1c0d9e8f7a6b5c4d3e
# compose 启停
docker-compose up -d
docker-compose logs -f
docker-compose down
官方文档
- Docker Registry 官方镜像
- Registry 部署配置参考
- Registry HTTP API V2
- docker-registry-ui(第三方图形界面)
- insecure-registries 说明
总结
- 企业镜像必须进私有仓库,公共仓库只用来拉基础镜像
registry官方镜像一条docker run就能跑,功能完整,缺的只是图形界面- http 私服必须写进
insecure-registries,改完daemon-reload+restart,端口要和实际部署一致 - push 前一定先 tag,名字必须是
仓库地址/镜像名:版本。少了地址前缀就推到 Docker Hub 去了,这是这条链路里最贵的错误 - tag 不复制数据,只是加引用,两个名字共享同一个 IMAGE ID
- 拿到 digest 比拿 tag 靠谱,tag 会被覆盖,digest 不会
- 改
daemon.json前先备份,起不来就把文件移走让 Docker 用默认配置启动