1. 为什么需要私有镜像仓库
在 Kubernetes、Docker 等容器化技术大规模落地的今天,镜像仓库已经成为基础设施中不可或缺的一环。虽然 Docker Hub 等公共仓库使用方便,但在企业生产环境中,直接依赖公共仓库会带来几个现实问题:
- 网络不稳定:从国内拉取 Docker Hub 镜像经常超时或速度极慢,影响发布效率。
- 安全合规:企业内部镜像可能包含专有代码、敏感配置,不适合存放在第三方平台。
- 版本可控:公共仓库中的镜像 tag 可能被覆盖或删除,无法保证生产环境拉取到的镜像与测试环境一致。
- 离线交付:在隔离网络(如内网、政务云)中,必须自建仓库才能完成镜像分发。
因此,搭建一套可靠的私有镜像仓库体系,是容器化落地的基础工程。本文从最轻量的 registry:2 讲起,逐步深入到企业级 Harbor,最后给出跨机房镜像同步与保留策略的完整配置样例。
2. 前置依赖:01(私有仓库基础)
在动手搭建之前,先明确几个基础概念和前置条件。
2.1 镜像仓库的组成
一个完整的镜像仓库体系通常包含三部分:
- 存储后端:镜像层数据实际存放的位置,可以是本地磁盘、NFS、S3、OSS 等。
- 认证与授权:控制谁能推送、谁能拉取,以及不同用户的操作权限。
- 元数据服务:记录镜像的 tag、digest、manifest 等索引信息。
registry:2 是 Docker 官方提供的轻量级镜像仓库实现,只包含存储和基础 API,认证需要额外配置。Harbor 则是在 registry 之上封装了完整的认证、RBAC、漏洞扫描、复制等企业级能力。
2.2 环境准备
本文所有示例基于以下环境:
- Docker Engine 20.10+
- Docker Compose v2
- 一台 Linux 服务器(CentOS 7.9 / Ubuntu 22.04 均可)
- 域名与证书(用于 TLS 配置,测试环境可用自签证书)
3. registry:2 基础搭建
3.1 最简部署
bash
# 创建数据目录
mkdir -p /data/registry
# 启动 registry:2
docker run -d \
--name registry \
-p 5000:5000 \
-v /data/registry:/var/lib/registry \
--restart=always \
registry:2
启动后,在 /etc/docker/daemon.json 中配置 insecure-registries(仅限 HTTP 或自签证书场景):
json
{
"insecure-registries": ["registry.example.com:5000"]
}
重启 Docker 使配置生效:
bash
systemctl restart docker
3.2 推送与拉取验证
bash
# 打 tag
docker tag nginx:latest registry.example.com:5000/nginx:1.25
# 推送
docker push registry.example.com:5000/nginx:1.25
# 拉取
docker pull registry.example.com:5000/nginx:1.25
3.3 坑位总结:推送 401 与认证配置
默认情况下 registry:2 是匿名可读写的,这在生产环境非常危险。配置 htpasswd 认证:
bash
# 生成密码文件
mkdir -p /data/registry-auth
docker run --rm --entrypoint htpasswd registry:2 \
-Bbn admin 'YourStrongPass' > /data/registry-auth/htpasswd
# 带认证启动
docker run -d \
--name registry \
-p 5000:5000 \
-v /data/registry:/var/lib/registry \
-v /data/registry-auth:/auth \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
--restart=always \
registry:2
此时未登录直接 push 会返回 401 Unauthorized,需要先登录:
bash
docker login registry.example.com:5000 -u admin -p 'YourStrongPass'
坑位提醒 :htpasswd 必须使用 -B(bcrypt)算法生成,否则 registry 会拒绝启动并报 htpasswd: invalid hash format。
3.4 TLS 配置
生产环境必须启用 HTTPS。假设已有证书 server.crt 和 server.key:
bash
docker run -d \
--name registry \
-p 443:5000 \
-v /data/registry:/var/lib/registry \
-v /data/certs:/certs \
-e "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt" \
-e "REGISTRY_HTTP_TLS_KEY=/certs/server.key" \
--restart=always \
registry:2
启用 TLS 后,daemon.json 中不再需要 insecure-registries,直接使用 https://registry.example.com 访问。
3.5 存储目录与 GC 机制
registry 的镜像数据默认存放在 /var/lib/registry/docker/registry/v2 下,目录结构如下:
text
v2/
├── blobs/ # 镜像层数据(按 digest 存储)
│ └── sha256/
├── repositories/ # 仓库与 tag 索引
│ └── nginx/
└── revisions/ # manifest 修订记录
坑位总结:GC 机制 。registry:2 的 docker registry garbage-collect 命令用于清理未被引用的 blob。但需要注意:
- GC 必须在停止写入的情况下执行,否则可能删除正在使用的层。
- 删除镜像 tag 后,blob 不会立即释放,需要手动触发 GC。
- GC 是阻塞式操作,大仓库执行时会导致 registry 短暂不可用。
bash
# 进入容器执行 GC(先停容器写入)
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
重要提醒:registry 原生的 tag 删除与 GC 机制并不适合生产环境频繁清理的场景,这也是很多团队转向 Harbor 的原因------Harbor 提供了更安全的保留策略和回收机制。
4. Harbor:企业级镜像仓库
Harbor 是基于 registry 的开源企业级镜像仓库,由 VMware 发起,现为 CNCF 毕业项目。它在 registry 之上补齐了认证、RBAC、漏洞扫描、复制、保留策略等能力。
4.1 部署 Harbor
以 Harbor 2.10 为例,使用离线安装包:
bash
# 下载并解压
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgz
tar xzf harbor-offline-installer-v2.10.0.tgz
cd harbor
# 复制配置模板
cp harbor.yml.tmpl harbor.yml
编辑 harbor.yml 核心配置:
yaml
hostname: harbor.example.com
http:
port: 80
https:
port: 443
certificate: /data/certs/server.crt
private_key: /data/certs/server.key
harbor_admin_password: 'Admin@123456'
database:
password: 'DB@123456'
data_volume: /data/harbor
执行安装:
bash
sudo ./install.sh --with-trivy --with-chartmuseum
--with-trivy 启用漏洞扫描,--with-chartmuseum 启用 Helm Chart 仓库。
4.2 项目与 RBAC
Harbor 通过「项目」隔离镜像,每个项目可设置不同的访问级别:
- 公开:所有人可拉取,无需登录。
- 私有:仅项目成员可访问。
RBAC 角色从高到低:
| 角色 | 权限 |
|---|---|
| 项目管理员 | 管理成员、配置策略、推送/拉取 |
| 开发者 | 推送/拉取镜像 |
| 访客 | 仅拉取 |
| 受限访客 | 仅拉取公开镜像 |
创建项目并添加成员:
bash
# 通过 API 创建项目
curl -u admin:Admin@123456 \
-X POST https://harbor.example.com/api/v2.0/projects \
-H "Content-Type: application/json" \
-d '{"project_name":"prod-app","metadata":{"public":"false"}}'
# 添加成员并赋予开发者角色
curl -u admin:Admin@123456 \
-X POST https://harbor.example.com/api/v2.0/projects/prod-app/members \
-H "Content-Type: application/json" \
-d '{"role_id":2,"member_user":{"username":"dev01"}}'
4.3 漏洞扫描
Harbor 集成 Trivy 扫描器,可对镜像进行 CVE 漏洞检测。扫描策略建议:
- 推送时自动扫描:在项目配置中开启「自动扫描图像」。
- 定时扫描:设置每日扫描,覆盖新披露的漏洞。
- 阻止漏洞镜像:可配置「阻止有严重漏洞的镜像被推送」。
在项目「配置管理」中开启:
yaml
# 项目配置(Web UI 或 API)
auto_scan: true
prevent_vul: true
severity: high
4.4 镜像复制(跨机房同步)
Harbor 的复制功能支持拉取模式 和推送模式,是实现双机房镜像同步的核心能力。
场景:主机房 Harbor → 备机房 Harbor
在备机房 Harbor 中创建「复制规则」,采用拉取模式从主机房同步:
yaml
# 复制规则配置(Web UI 或 API)
name: sync-from-primary
src_registry:
id: 1 # 主机房 Harbor 的注册 ID
type: harbor
endpoint: https://harbor-primary.example.com
credential:
access_key: robot$sync
access_secret: 'RobotToken123'
src_namespace: prod-app
dest_namespace: prod-app
filters:
- type: resource
value: image
- type: tag
value: 'v*'
trigger:
type: event_based # 事件触发,也可用 scheduled 定时
坑位提醒 :跨机房复制建议使用机器人账户(Robot Account)而非普通用户,权限更可控且支持密钥轮换。
完整配置样例:双机房复制 + 保留策略
以下是一个生产可用的完整配置组合:
yaml
# 主机房 Harbor(harbor-primary)
# 项目 prod-app 配置:
# 1. 自动扫描开启
# 2. 保留策略:保留最近 30 个 tag,且保留所有带 prod 后缀的 tag
# 3. 复制规则:事件触发推送到备机房
# 备机房 Harbor(harbor-dr)
# 项目 prod-app 配置:
# 1. 复制规则:拉取模式,从主机房同步
# 2. 保留策略:保留最近 60 个 tag(比主机房多,防止主备切换时丢镜像)
保留策略配置(Web UI 或 API):
yaml
# 保留规则示例
rules:
- action: retain
scope: repository
repository: prod-app/*
tag_count: 30
tag_patterns:
- '*-prod' # 带 prod 后缀的 tag 永久保留
untagged: false
4.5 保留策略实战
Harbor 的保留策略(Retention Policy)用于自动清理过期镜像,避免存储无限增长。常见策略:
- 按数量保留:每个仓库保留最近 N 个 tag。
- 按时间保留:保留最近 N 天的镜像。
- 按 tag 规则保留 :匹配特定模式的 tag 永久保留(如
*-prod、*-release)。 - 保留带 digest 的:防止被 GC 误删。
坑位总结:保留策略与 GC 的配合。Harbor 执行保留策略后,被删除的 tag 对应的 blob 不会立即释放,需要等待 Harbor 的 GC 任务执行。建议在非业务高峰期手动触发 GC:
bash
# 通过 API 触发 GC
curl -u admin:Admin@123456 \
-X POST https://harbor.example.com/api/v2.0/system/gc \
-H "Content-Type: application/json" \
-d '{"schedule":{"type":"Manual"}}'
5. 公网拉取加速与镜像代理
5.1 国内镜像源
在国内直接拉取 Docker Hub 镜像经常失败或极慢,常见解决方案:
- 配置 Docker 镜像加速器(阿里云、腾讯云等提供的加速地址)。
- 使用代理仓库:在 Harbor 中配置「代理缓存」项目,指向 Docker Hub 或国内镜像源。
Harbor 代理缓存项目配置:
yaml
# 创建代理缓存项目
project_name: dockerhub-proxy
registry_proxy:
endpoint: https://registry-1.docker.io
type: docker-hub
之后拉取 dockerhub-proxy/library/nginx:1.25 即可,Harbor 会自动从 Docker Hub 拉取并缓存到本地。
5.2 阿里云 ACR 与镜像代理
阿里云容器镜像服务(ACR)提供个人版和企业版,支持:
- 镜像加速:内置国内加速节点。
- 跨地域同步:类似 Harbor 的复制功能。
- 公网与 VPC 访问:企业版支持专有网络内网拉取。
坑位总结:镜像源失效切换。国内镜像源经常变动,建议:
- 配置多个加速器地址,Docker 会依次尝试。
- 关键镜像提前同步到私有仓库,避免依赖公共源。
- 使用 Harbor 代理缓存时,定期验证上游源可用性。
json
{
"registry-mirrors": [
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com",
"https://mirror.baidubce.com"
]
}
6. 镜像 tag 规范
统一的 tag 规范是镜像可追溯、可回滚的基础。推荐组合:语义化版本 + git sha + 环境后缀。
6.1 规范格式
text
<应用名>:<语义化版本>-<git短sha>-<环境后缀>
示例:
text
order-service:1.4.2-a1b2c3d-prod
order-service:1.4.2-a1b2c3d-staging
order-service:1.4.2-a1b2c3d-dev
6.2 各字段说明
| 字段 | 说明 | 示例 |
|---|---|---|
| 语义化版本 | 主版本.次版本.修订号,遵循 SemVer | 1.4.2 |
| git sha | 构建时的 git 提交短哈希,保证可追溯 | a1b2c3d |
| 环境后缀 | 标识部署环境 | prod / staging / dev |
6.3 配套 CI 实践
在 CI 中自动生成 tag:
bash
# GitLab CI / Jenkins 示例
VERSION=$(cat VERSION) # 如 1.4.2
GIT_SHA=$(git rev-parse --short HEAD)
ENV_SUFFIX=${DEPLOY_ENV:-dev}
IMAGE_TAG="${VERSION}-${GIT_SHA}-${ENV_SUFFIX}"
docker build -t harbor.example.com/prod-app/order-service:${IMAGE_TAG} .
docker push harbor.example.com/prod-app/order-service:${IMAGE_TAG}
坑位提醒 :latest tag 只适合开发环境,生产环境必须使用不可变的完整 tag,否则无法精确定位线上运行的镜像版本。
7. 完整配置样例:Harbor 双机房复制 + 保留策略
下面给出一个可直接落地的完整配置组合,覆盖双机房高可用场景。
7.1 架构说明
#mermaid-svg-a5yBcHhz6n0IThqT{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-a5yBcHhz6n0IThqT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-a5yBcHhz6n0IThqT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-a5yBcHhz6n0IThqT .error-icon{fill:#552222;}#mermaid-svg-a5yBcHhz6n0IThqT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-a5yBcHhz6n0IThqT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-a5yBcHhz6n0IThqT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-a5yBcHhz6n0IThqT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-a5yBcHhz6n0IThqT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-a5yBcHhz6n0IThqT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-a5yBcHhz6n0IThqT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-a5yBcHhz6n0IThqT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-a5yBcHhz6n0IThqT .marker.cross{stroke:#333333;}#mermaid-svg-a5yBcHhz6n0IThqT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-a5yBcHhz6n0IThqT p{margin:0;}#mermaid-svg-a5yBcHhz6n0IThqT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-a5yBcHhz6n0IThqT .cluster-label text{fill:#333;}#mermaid-svg-a5yBcHhz6n0IThqT .cluster-label span{color:#333;}#mermaid-svg-a5yBcHhz6n0IThqT .cluster-label span p{background-color:transparent;}#mermaid-svg-a5yBcHhz6n0IThqT .label text,#mermaid-svg-a5yBcHhz6n0IThqT span{fill:#333;color:#333;}#mermaid-svg-a5yBcHhz6n0IThqT .node rect,#mermaid-svg-a5yBcHhz6n0IThqT .node circle,#mermaid-svg-a5yBcHhz6n0IThqT .node ellipse,#mermaid-svg-a5yBcHhz6n0IThqT .node polygon,#mermaid-svg-a5yBcHhz6n0IThqT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-a5yBcHhz6n0IThqT .rough-node .label text,#mermaid-svg-a5yBcHhz6n0IThqT .node .label text,#mermaid-svg-a5yBcHhz6n0IThqT .image-shape .label,#mermaid-svg-a5yBcHhz6n0IThqT .icon-shape .label{text-anchor:middle;}#mermaid-svg-a5yBcHhz6n0IThqT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-a5yBcHhz6n0IThqT .rough-node .label,#mermaid-svg-a5yBcHhz6n0IThqT .node .label,#mermaid-svg-a5yBcHhz6n0IThqT .image-shape .label,#mermaid-svg-a5yBcHhz6n0IThqT .icon-shape .label{text-align:center;}#mermaid-svg-a5yBcHhz6n0IThqT .node.clickable{cursor:pointer;}#mermaid-svg-a5yBcHhz6n0IThqT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-a5yBcHhz6n0IThqT .arrowheadPath{fill:#333333;}#mermaid-svg-a5yBcHhz6n0IThqT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-a5yBcHhz6n0IThqT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-a5yBcHhz6n0IThqT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a5yBcHhz6n0IThqT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-a5yBcHhz6n0IThqT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a5yBcHhz6n0IThqT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-a5yBcHhz6n0IThqT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-a5yBcHhz6n0IThqT .cluster text{fill:#333;}#mermaid-svg-a5yBcHhz6n0IThqT .cluster span{color:#333;}#mermaid-svg-a5yBcHhz6n0IThqT 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-a5yBcHhz6n0IThqT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-a5yBcHhz6n0IThqT rect.text{fill:none;stroke-width:0;}#mermaid-svg-a5yBcHhz6n0IThqT .icon-shape,#mermaid-svg-a5yBcHhz6n0IThqT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a5yBcHhz6n0IThqT .icon-shape p,#mermaid-svg-a5yBcHhz6n0IThqT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-a5yBcHhz6n0IThqT .icon-shape .label rect,#mermaid-svg-a5yBcHhz6n0IThqT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a5yBcHhz6n0IThqT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-a5yBcHhz6n0IThqT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-a5yBcHhz6n0IThqT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 推送镜像
事件触发复制
拉取
拉取
保留策略:30 个 tag
保留策略:60 个 tag
CI/CD 流水线
主机房 Harbor
harbor-primary
备机房 Harbor
harbor-dr
K8s 集群(主)
K8s 集群(备)
自动清理旧镜像
自动清理旧镜像
7.2 主机房配置
yaml
# harbor-primary 配置
# 1. 项目 prod-app:私有,自动扫描开启
# 2. 复制规则:推送模式,目标为备机房
# 3. 保留策略:保留 30 个 tag,prod 后缀永久保留
# 复制规则(推送模式)
name: push-to-dr
src_registry:
type: local
dest_registry:
id: 2
type: harbor
endpoint: https://harbor-dr.example.com
credential:
access_key: robot$sync
access_secret: 'RobotToken456'
dest_namespace: prod-app
filters:
- type: resource
value: image
- type: tag
value: '*-prod'
trigger:
type: event_based
7.3 备机房配置
yaml
# harbor-dr 配置
# 1.