云仓库有免费额度、有网速、还有「镜像不能出机房」的合规要求。这篇把仓库建在自己的服务器上:先用一条命令起
registry,把 tag/push/pull 和_catalog接口跑通;再上harbor,从离线包、harbor.yml到install.sh装完十个容器,做用户、项目、角色授权,最后配一条跨机房同步规则并学会干净卸载。
本文概要:
| 项目 | 内容 |
|---|---|
| 文章主题 | 用 registry 与 harbor 自建 Docker 镜像仓库,含部署、授权、跨仓库同步与卸载 |
| 所属类别 | 云计算 → 容器技术 → 镜像仓库 |
| 知识关键字 | registry · harbor · insecure-registries · registry-mirrors · _catalog · docker compose · 复制规则 · extra_hosts |
主线一句:先跑通再谈管理。第一章两条命令起 registry,第二章打通推拉链路,第三、四章学会查和删。第五章换成带界面和权限的 harbor,第六章配同步,第七章卸载。
一、registry:一条命令起一个仓库
官方 registry 镜像自带一个完整仓库,跑起来就行:
bash
# 宿主 80 → 容器 5000;/registry 挂出来,容器删了镜像还在
[root@docker ~]# docker container run -d -p 80:5000 --restart=always \
--name registry -v /registry:/var/lib/registry registry
56f70abaf1d08f5cfc5c659c74c5922ecc9a77c528a59c5fefb08391582f6e55
六个参数各管一件事:
| 参数 | 含义 | 英文原意 |
|---|---|---|
-d |
后台运行,不占终端 | detach,分离 |
-p 80:5000 |
宿主 80 映射容器 5000,registry 在容器里监听 5000 | publish,发布端口 |
--name registry |
给容器起名,之后能用名字停删 | name,名字 |
-v /registry:/var/lib/registry |
宿主目录挂进容器,镜像数据落在宿主机 | volume,卷 |
--restart=always |
Docker 重启后容器自己回来 | restart,重启 |
registry |
镜像名,官方最小仓库镜像 | --- |
--restart=always 让它在 Docker 重启后自己回来,-v /registry:/var/lib/registry 是关键------不挂这个目录,容器一删,所有镜像跟着没。
1.1 先让 Docker 允许走 http
Docker 1.9 之后默认只认 https,访问 http 仓库会被拒绝。内部仓库没必要为它签证书,两端都放开即可:
bash
[root@docker ~]# tee /etc/docker/daemon.json <<-'EOF'
{
"insecure-registries": ["registry.zhb.cloud"],
"registry-mirrors": ["https://<你的加速器地址>.mirror.swr.myhuaweicloud.com"]
}
EOF
[root@docker ~]# systemctl restart docker
两件事:insecure-registries 明确允许这个域名走 http(可以带端口,registry.zhb.cloud:5000),registry-mirrors 是拉公共镜像的加速器------两者互不相干,别写混。
域名要能解析到本机,否则 Docker 连不上:
bash
[root@docker ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain
10.1.8.10 registry.zhb.cloud
⚠️ 注意 :
insecure-registries只写域名(可带端口)就够,官方示例是"myregistrydomain.com:5000"。网上不少教程写成"http://registry.zhb.cloud",Docker 也能容忍,但一旦带上路径或多余斜杠就会解析失败,重启 docker 直接报错。
二、把镜像推上去、拉下来
registry 没有「组织」概念,镜像名的第二段随便写,仓库里就按这个路径存。
bash
# 三类镜像各推一个,路径按用途分类
[root@docker ~]# docker tag busybox registry.zhb.cloud/tools/busybox:latest
[root@docker ~]# docker push registry.zhb.cloud/tools/busybox:latest
[root@docker ~]# docker tag wordpress registry.zhb.cloud/webapp/wordpress:latest
[root@docker ~]# docker push registry.zhb.cloud/webapp/wordpress:latest
[root@docker ~]# docker tag mysql registry.zhb.cloud/db/mysql:latest
[root@docker ~]# docker tag mysql registry.zhb.cloud/db/mysql:test
[root@docker ~]# docker push registry.zhb.cloud/db/mysql:latest
[root@docker ~]# docker push registry.zhb.cloud/db/mysql:test
同一个镜像贴两个标签,就是仓库里两个版本。test 和 latest 指向同一份内容,但地址不同------回滚时切标签比重新推一份快得多。
bash
# 拉回来,速度是内网的速度
[root@docker ~]# docker pull registry.zhb.cloud/webapp/wordpress:latest
[root@docker ~]# docker pull registry.zhb.cloud/db/mysql:latest
2.1 让 docker pull db/mysql 也命中本地仓库
平时敲 docker pull mysql,Docker 会去 docker.io。把私有仓库塞进 registry-mirrors 列表,短名字也能优先命中本地:
bash
[root@docker ~]# tee /etc/docker/daemon.json <<-'EOF'
{
"insecure-registries": ["registry.zhb.cloud"],
"registry-mirrors": ["http://registry.zhb.cloud","https://<你的加速器地址>.mirror.swr.myhuaweicloud.com"]
}
EOF
[root@docker ~]# systemctl restart docker
[root@docker ~]# docker pull db/mysql
Status: Downloaded newer image for db/mysql:latest
docker.io/db/mysql:latest
回显里写着 docker.io/db/mysql:latest,但下载是从列表里第一个地址拿的,所以几乎瞬间完成。这是利用 mirror 的兜底行为的加速手段,不是权限控制------公开仓库里的同名镜像依然能被人拉到,别拿它当访问限制。
三、不装界面,用 curl 查仓库内容
registry 自带一套 HTTP API,查目录、查标签两条就够:
| 端点 | 作用 | 说明 |
|---|---|---|
/v2/_catalog |
列出所有仓库 | registry 本身不支持分页过滤,仓库多了会很慢 |
/v2/<名字>/tags/list |
列出某个仓库的标签 | 名字带斜杠要原样写,如 db/mysql |
/v2/ |
探活 | 返回 {} 说明服务正常,401 说明要认证 |
bash
[root@docker ~]# curl -s http://registry.zhb.cloud/v2/_catalog
{"repositories":["db/mysql","tools/busybox","webapp/wordpress"]}
# 装个 yajl 让 JSON 好看点,排查时省眼力
[root@docker ~]# yum install -y yajl
[root@docker ~]# curl -s http://registry.zhb.cloud/v2/db/mysql/tags/list | json_reformat
{
"name": "db/mysql",
"tags": [
"latest",
"test"
]
}
⚠️ 注意 :registry 的 API 是明文 http 且默认无认证(除非另配 htpasswd)。任何人只要能访问 80 端口,就能列出你所有镜像并拉走。这台机器必须只在内网,公网部署请直接上 harbor。
四、删镜像:registry 不给你 rmi
docker rmi 只删本地,仓库里的 blob 和清单还在。registry 的删除要走它自带的脚本:
bash
# 下载社区维护的清理脚本
[root@docker ~]# curl -s https://raw.githubusercontent.com/burnettk/delete-docker-registry-image/master/delete_docker_registry_image.py | sudo tee /usr/local/bin/delete_docker_registry_image >/dev/null
[root@docker ~]# chmod a+x /usr/local/bin/delete_docker_registry_image
# 告诉脚本仓库数据在哪(宿主上挂的那个目录)
[root@docker ~]# export REGISTRY_DATA_DIR=/registry/docker/registry/v2
| 参数 | 含义 | 英文原意 |
|---|---|---|
-i / --image |
要删的镜像路径,如 webapp/wordpress |
image,镜像 |
-n / --dry-run |
只打印要删哪些文件,不动手 | dry run,空跑 |
-p / --prune |
顺带清掉无人引用的 blob | prune,修剪 |
-u / --untagged |
删掉该镜像下所有没有标签的 blob | untagged,未打标签 |
-v / --verbose |
打印详细过程 | verbose,啰嗦 |
bash
# 先空跑看一眼,确认无误再真删
[root@docker ~]# delete_docker_registry_image -i webapp/wordpress -n
[root@docker ~]# delete_docker_registry_image -i webapp/wordpress
INFO Deleting /data/docker/registry/v2/blobs/sha256/46/4676067592f277e1723e4ab4c588603df0b3dea762e22c354f7ada29b391cf10
INFO Deleting /data/docker/registry/v2/repositories/webapp/wordpress
[root@docker ~]# curl -s http://registry.zhb.cloud/v2/_catalog | json_reformat
{
"repositories": [
"db/mysql",
"tools/busybox"
]
}
删完目录就空了。-n 是这条命令唯一的安全带,养成先空跑的习惯。
五、harbor:带界面、带权限的仓库
registry 只能存镜像,谁推的、谁拉的、能不能删,一概不知道。harbor 把这几件事补齐了。
| 能力 | registry | harbor |
|---|---|---|
| 网页界面 | 无 | 有,能浏览仓库、看标签、看日志 |
| 用户与权限 | 无 | 用户、项目、五种角色 |
| 操作审计 | 无 | 记录每次推拉删 |
| 镜像扫描 | 无 | 集成 Trivy 漏洞扫描 |
| 部署方式 | 一个容器 | 十个容器,靠 docker compose 编排 |
| 镜像清理 | 手写脚本 | 界面里配保留策略 |
5.1 装:离线包 + 改三处配置 + install.sh
bash
[root@harbor ~]# wget https://github.com/goharbor/harbor/releases/download/v2.10.2/harbor-offline-installer-v2.10.2.tgz
[root@harbor ~]# tar -xf harbor-offline-installer-v2.10.2.tgz
[root@harbor ~]# cd harbor/
[root@harbor harbor]# ls
common.sh harbor.v2.10.2.tar.gz harbor.yml.tmpl install.sh LICENSE prepare
[root@harbor harbor]# cp harbor.yml.tmpl harbor.yml
harbor.yml.tmpl 是模板,复制一份改成 harbor.yml 再改,别直接动模板。要改的只有三处:
yaml
# 1) 访问域名,必须和客户端 /etc/hosts 里的一致
hostname: registry.zhb.cloud
# 2) https 段整段注释掉,内部用 http
# https:
# port: 443
# certificate: /your/certificate/path
# private_key: /your/private/key/path
# 3) 管理员初始密码,上线前一定要改
harbor_admin_password: Zhb@123
bash
# 本机 hosts 也要有解析记录
[root@harbor ~]# echo "10.1.8.10 registry.zhb.cloud" >> /etc/hosts
[root@harbor harbor]# ./install.sh
[Step 0]: checking if docker is installed ...
Note: docker version: 26.1.3
[Step 1]: checking docker-compose is installed ...
Note: Docker Compose version v2.27.0
[Step 2]: loading Harbor images ...
Loaded image: goharbor/prepare:v2.10.2
[Step 3]: preparing environment ...
[Step 4]: preparing harbor configs ...
Generated configuration file: /compose_location/docker-compose.yml
[Step 5]: starting Harbor ...
[+] Running 10/10
✔ Container harbor-log Started
✔ Container registryctl Started
✔ Container harbor-db Started
✔ Container redis Started
✔ Container registry Started
✔ Container harbor-portal Started
✔ Container harbor-core Started
✔ Container harbor-jobservice Started
✔ Container nginx Started
✔ ----Harbor has been installed and started successfully.----
十步输出里要盯的是 [Step 4] 和 [Step 5]:前者生成 docker-compose.yml,后者把十个容器拉起来。中间出现 WARNING: HTTP protocol is insecure 属正常,因为你确实关了 https。
bash
# 十个容器全 healthy 才叫装好
[root@harbor ~]# docker ps --format='{{ .Names }}\t{{ .Status }}'
harbor-jobservice Up 28 minutes (healthy)
nginx Up 28 minutes (healthy)
harbor-core Up 28 minutes (healthy)
redis Up 28 minutes (healthy)
harbor-db Up 28 minutes (healthy)
registry Up 28 minutes (healthy)
harbor-portal Up 28 minutes (healthy)
registryctl Up 28 minutes (healthy)
harbor-log Up 28 minutes (healthy)
5.2 注册成系统服务
install.sh 起的容器只是「跑起来了」,机器重启不会自己回来。补一个 unit:
bash
# 把整个目录搬到 /etc,配置和 compose 文件跟着走
[root@harbor ~]# cp -a harbor /etc
[root@harbor ~]# vim /etc/systemd/system/harbor.service
ini
[Unit]
Description=Harbor Docker Registry
After=docker.service network-online.target
Requires=docker.service network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/etc/harbor
ExecStart=/usr/libexec/docker/cli-plugins/docker-compose up -d
ExecStop=/usr/libexec/docker/cli-plugins/docker-compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
bash
[root@harbor ~]# systemctl daemon-reload
[root@harbor ~]# systemctl enable harbor
Type=oneshot + RemainAfterExit=yes 是这套写法的关键:docker compose up -d 起完就退出,oneshot 才不会把它当成「服务挂了」反复重启。
5.3 网页端:建用户、建项目、授权
浏览器打开 http://registry.zhb.cloud/:

用 admin 和 harbor.yml 里那个密码登进去。先建普通用户,再建项目,最后把用户加进项目------顺序反过来会没地方授权。

项目名就是镜像名里的第二段,公开项目任何人可拉、私有项目要授权。配额按存储算,实验里 10GiB 足够。
授权在项目的「成员」页里加:

五种角色的边界很清楚:
| 角色 | 能做什么 |
|---|---|
| 项目管理员 | 项目内全部权限,含加成员、改配置、删仓库 |
| 维护人员 | 推拉镜像、扫描、看日志,不能管成员 |
| 开发者 | 推拉镜像,不能删仓库、不能扫描 |
| 访客 | 只读,只能拉 |
| 受限访客 | 只读且看不到日志 |
5.4 客户端登录:凭据存哪、怎么改
bash
[root@docker ~]# tee /etc/docker/daemon.json <<-'EOF'
{
"insecure-registries": ["registry.zhb.cloud"],
"registry-mirrors": ["http://registry.zhb.cloud","https://<你的加速器地址>.mirror.swr.myhuaweicloud.com"]
}
EOF
[root@docker ~]# systemctl restart docker
⚠️ 注意 :如果 harbor 和客户端是同一台机器,
systemctl restart docker会把 harbor 的十个容器一起停掉。重启完回 harbor 目录再跑一次./install.sh,容器才会回来。
bash
[root@docker ~]# docker login registry.zhb.cloud
Username: zhb
Password:
Login Succeeded
# 凭据落在用户家目录,内容是 base64
[root@docker ~]# cat ~/.docker/config.json
{
"auths": {
"registry.zhb.cloud": {
"auth": "emhiOlpoYkAxMjM="
}
}
}
那一串 emhiOlpoYkAxMjM= 就是 用户名:密码 的 base64,等于明文,只是看着不像:
bash
[root@docker ~]# echo -n 'zhb:Zhb@123' | base64
emhiOlpoYkAxMjM=
镜像地址格式是 服务器/项目/镜像:标签,推之前把项目段写对:
bash
# 推到 cloud 项目下
[root@docker ~]# docker tag mysql registry.zhb.cloud/cloud/mysql:latest
[root@docker ~]# docker push registry.zhb.cloud/cloud/mysql:latest
[root@docker ~]# docker push registry.zhb.cloud/cloud/wordpress:latest
# 拉
[root@docker ~]# docker pull registry.zhb.cloud/cloud/mysql:latest
# 查仓库清单要管理员凭据
[root@docker ~]# curl -s http://registry.zhb.cloud/v2/_catalog -u admin:Zhb@123
{"repositories":["cloud/mysql"]}
和裸 registry 最大的区别就在这两行 curl 上:带了 -u,说明这套 API 后面已经有了认证。
5.5 跨仓库同步:把镜像搬到另一台 harbor
场景是两个机房各有一套 harbor,需要把镜像从 A 同步到 B。harbor 把这件事拆成「仓库管理」和「复制规则」两步:前者告诉它目标在哪,后者告诉它搬什么。
第一步:仓库管理 → 新建目标

| 字段 | 填什么 |
|---|---|
| 提供者 | Harbor(同步 harbor 就选它,别的 registry 选 Docker Registry) |
| 目标名 | 自定义,只给运维看,如 harbor-prod |
| 目标 URL | 远端 harbor 的访问地址 |
| 访问 ID / 密码 | 远端 harbor 的管理员账号 |
| 验证远程证书 | 目标用自签证书时取消勾选,否则连不上 |
⚠️ 注意 :如果远端 harbor 的
hostname写的是 FQDN(如hub.zhb.cloud),本机必须能解析这个名字,否则同步任务会卡在「连接失败」。要么加 DNS,要么在本地 harbor 上补extra_hosts。
yaml
# /etc/harbor/docker-compose.override.yml(本地 harbor 上)
services:
core:
extra_hosts:
- "hub.zhb.cloud:192.168.50.201"
jobservice:
extra_hosts:
- "hub.zhb.cloud:192.168.50.201"
第二步:复制管理 → 新建规则

| 字段 | 取值 | 说明 |
|---|---|---|
| 名称 | sync-all |
必填,运维识别用,不能重名 |
| 复制模式 | Pull-based | 由当前 harbor 主动去源拉;Push-based 是源端主动推 |
| 源仓库 | harbor-prod |
上一步建的目标 |
| 源资源过滤器 | 留空 / 全部 | 留空即同步所有项目、所有标签 |
| 目标名称空间 | 留空 | 留空则与源端同名;填了就全塞进这一个项目 |
| 仓库扁平化 | 替换 1 级 | 去掉路径第一层,路径变了业务拉取地址要跟着改 |
| 触发模式 | 手动 | 也可定时(Cron)或事件驱动(源端一 push 就同步) |
第三步:选中规则点「复制」,看任务结果

手动模式保存后不会自己跑,必须点「复制」。任务列表里状态是 Succeeded 才算搬完,Failed 就点进去看报错------九成是网络不通或证书校验没关。
六、卸载:四步清干净
bash
# 1. 停容器
[root@harbor harbor]# docker compose down
# 2. 删镜像
[root@harbor harbor]# docker images | grep harbor | awk '{print $1":"$2}' | xargs docker rmi
# 3. 删数据目录(镜像、数据库、密钥都在这)
[root@harbor harbor]# rm -fr /data
# 4. 删安装包
[root@harbor ~]# rm -fr harbor harbor-offline-installer-v2.10.2.tgz
第 3 步是重点:/data 里是全部镜像和 harbor 的数据库,删了就真没了。只想清理空间、还想留配置的,跳过第 3 步。
顺手把 systemd 的残留也清掉,否则下次装会撞名:
bash
[root@harbor ~]# systemctl disable harbor
[root@harbor ~]# rm -f /etc/systemd/system/harbor.service
[root@harbor ~]# systemctl daemon-reload
附:命令英文全称速查表
| 命令 / 缩写 | 英文全称 / 原意 | 作用 |
|---|---|---|
registry |
registry | 官方最小镜像仓库 |
harbor |
harbor | 企业级镜像仓库,含界面与权限 |
login / logout |
login / logout | 登录、退出仓库 |
push / pull |
push / pull | 推、拉镜像 |
compose |
compose | 按 compose 文件编排多容器 |
_catalog |
catalog | 仓库清单 API 端点 |
replication |
replication | 复制,harbor 的跨仓库同步 |
extra_hosts |
extra hosts | compose 里给容器额外写 hosts |
Trivy |
Trivy | 镜像漏洞扫描组件 |
📚 参考资料(官方文档)
- registry 官方镜像与部署说明:https://distribution.github.io/distribution/
- registry 配置项(
insecure-registries等):https://distribution.github.io/distribution/about/configuration/ - Harbor 安装与配置指南:https://goharbor.io/docs/2.10.0/install-config/
- Harbor 复制(跨仓库同步)配置:https://goharbor.io/docs/2.10.0/administration/configuring-replication/
总结 :内网要一个能用的仓库,
registry一个容器 + 一份daemon.json就够,代价是没有权限、日志和清理------所以它只适合实验或严格内网。要给人用、要给团队用,直接上 harbor:harbor.yml改域名、关 https、改密码三处,install.sh一跑,十个容器起来,再配用户、项目、角色,最后按需配复制规则把镜像搬到另一个机房。记住两个坑:重启 docker 会带走同机的 harbor,删镜像必须用脚本而不是docker rmi。
📚 同系列 · Docker 从镜像到容器(其余篇目见博客主页)
- 上一篇:《镜像推上云:华为云 SWR 与阿里云 ACR 的建组织、加速器、上传与公开》
- 相关篇:《别再 docker commit 造镜像:Dockerfile 十六个指令与 docker build 实操》
- 相关篇:《Docker 镜像管理:十个命令走完找拿看标删搬推》
💬 你的 harbor 装完打不开首页,一般是 nginx 容器没起来。docker compose ps 看一眼,把 harbor-log 的日志贴出来,一起定位是端口占用还是证书配置。
