云上容器镜像仓库选型:Harbor vs 云厂商 ACR vs GHCR,企业级镜像管理怎么选?

阿里云上云采购季 2核2G云服务器99元/年 续费同价

当你的 Docker 镜像从 10 个涨到 1000 个,当你需要在多个集群之间同步镜像,当你需要对镜像做安全扫描和版本管理——你需要一个靠谱的容器镜像仓库。本文对比三主流方案:开源的 Harbor、云厂商托管 ACR、GitHub Container Registry,帮你根据团队规模和场景做出最优选择。


1. 三大方案核心对比

对比维度 Harbor 腾讯云 TCR / 阿里云 ACR GitHub Container Registry (GHCR)
部署方式 自建(Docker/K8s) 云托管(开箱即用) 云托管(与 GitHub 深度集成)
费用 免费(需自备服务器) 按存储+请求量付费 公开仓库免费,私有仓库按流量
安全扫描 Trivy 集成 云原生漏洞扫描 Dependabot 集成
多区域同步 支持(Replication) 全球加速自动同步 不支持
CI/CD 集成 需自行配置 与云厂商 CI/CD 深度集成 GitHub Actions 原生支持
镜像签名 Notary 支持 KMS 密钥管理 Cosign 支持
适合团队规模 50+ 人的中大型团队 任何规模 开源项目/小型团队

2. 方案一:Harbor 自建(适合中大型团队)

Harbor 是 CNCF 毕业项目,功能最全面,但需要自己维护。

Docker Compose 快速部署

# docker-compose.yml — Harbor 2.12 部署
version: "3.8"

services:
  harbor-db:
    image: goharbor/harbor-db:v2.12.0
    container_name: harbor-db
    restart: always
    environment:
      POSTGRES_PASSWORD: harbor123
      POSTGRES_USER: postgres
      POSTGRES_DB: registry
    volumes:
      - harbor_db:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    container_name: harbor-redis
    restart: always

  harbor-core:
    image: goharbor/harbor-core:v2.12.0
    container_name: harbor-core
    restart: always
    environment:
      CORE_SECRET: notsecure
      JOBS_SERVICE_SECRET: notsecure
      EXT_ENDPOINT: https://harbor.yourdomain.com
      DATABASE_TYPE: postgresql
      POSTGRESQL_HOST: harbor-db
      POSTGRESQL_PORT: 5432
      POSTGRESQL_DATABASE: registry
      POSTGRESQL_USERNAME: postgres
      POSTGRESQL_PASSWORD: harbor123
      REGISTRY_CONTROLLER_URL: http://harbor-registry:8080
      PORTAL_URL: http://harbor-portal:8080
      TOKEN_SERVICE_URL: http://harbor-core:8080/service/token
      REGISTRY_URL: http://harbor-registry:5000
    depends_on:
      - harbor-db
      - redis
      - harbor-registry

  registry:
    image: goharbor/registry-photon:v2.12.0
    container_name: harbor-registry
    restart: always
    volumes:
      - registry_data:/storage

  portal:
    image: goharbor/harbor-portal:v2.12.0
    container_name: harbor-portal
    restart: always

  trivy:
    image: aquasec/trivy:0.50.0
    container_name: harbor-trivy
    restart: always

volumes:
  harbor_db:
  registry_data:

Harbor 自动化镜像管理脚本

# harbor_manager.py — Harbor 镜像仓库管理脚本
import requests
import json
from datetime import datetime
from typing import Optional

class HarborManager:
    """Harbor 镜像仓库管理"""

    def __init__(self, url: str, username: str, password: str):
        self.base_url = url.rstrip("/")
        self.session = requests.Session()
        self.session.auth = (username, password)
        self.session.headers.update({"Content-Type": "application/json"})
        # 获取认证 token
        self._auth()

    def _auth(self):
        """获取 Harbor JWT token"""
        resp = self.session.get(f"{self.base_url}/api/v2.0/users/current")
        resp.raise_for_status()
        self.token = resp.json().get("username", "")

    def list_projects(self) -> list:
        """列出所有项目"""
        resp = self.session.get(f"{self.base_url}/api/v2.0/projects")
        resp.raise_for_status()
        return [{"name": p["name"], "repo_count": p.get("repo_count", 0)}
                for p in resp.json()]

    def list_repositories(self, project: str) -> list:
        """列出项目下的所有镜像仓库"""
        resp = self.session.get(
            f"{self.base_url}/api/v2.0/projects/{project}/repositories"
        )
        resp.raise_for_status()
        return [r["name"] for r in resp.json()]

    def list_artifacts(self, project: str, repo: str) -> list:
        """列出镜像的所有 tag"""
        resp = self.session.get(
            f"{self.base_url}/api/v2.0/projects/{project}/repositories"
            f"/{repo}/artifacts"
        )
        resp.raise_for_status()
        artifacts = []
        for a in resp.json():
            tags = [t["name"] for t in a.get("tags", [])]
            size_mb = round(a.get("size", 0) / 1024 / 1024, 1)
            push_time = a.get("push_time", "")
            artifacts.append({
                "digest": a["digest"][:16],
                "tags": tags or ["<none>"],
                "size_mb": size_mb,
                "push_time": push_time[:10],
            })
        return artifacts

    def cleanup_old_images(self, project: str, repo: str,
                          keep_tags: int = 5) -> list:
        """清理旧镜像,保留最新的 N 个 tag"""
        artifacts = self.list_artifacts(project, repo)
        if len(artifacts) <= keep_tags:
            return []

        # 按推送时间排序
        artifacts.sort(key=lambda x: x.get("push_time", ""), reverse=True)
        to_delete = artifacts[keep_tags:]
        deleted = []

        for art in to_delete:
            for tag in art["tags"]:
                if tag == "<none>":
                    # 通过 digest 删除无 tag 的镜像
                    try:
                        self.session.delete(
                            f"{self.base_url}/api/v2.0/projects/{project}"
                            f"/repositories/{repo}/artifacts/{art['digest']}"
                        )
                        deleted.append(f"删除 digest: {art['digest'][:16]}")
                    except Exception as e:
                        deleted.append(f"失败: {art['digest'][:16]} ({e})")
                else:
                    try:
                        self.session.delete(
                            f"{self.base_url}/api/v2.0/projects/{project}"
                            f"/repositories/{repo}/artifacts/{tag}"
                        )
                        deleted.append(f"删除 tag: {tag}")
                    except Exception as e:
                        deleted.append(f"失败: {tag} ({e})")
        return deleted

    def get_quota_usage(self, project: str) -> dict:
        """获取项目存储配额使用情况"""
        resp = self.session.get(
            f"{self.base_url}/api/v2.0/projects/{project}/quotas"
        )
        quotas = resp.json()
        usage = {"hard": 0, "used": 0, "usage_percent": 0}
        for q in quotas:
            if q.get("hard"):
                usage["hard"] = q["hard"]["storage"]
                usage["used"] = q.get("used", {}).get("storage", 0)
                usage["usage_percent"] = round(
                    usage["used"] / usage["hard"] * 100, 1
                ) if usage["hard"] > 0 else 0
        return usage

    def scan_image(self, project: str, repo: str, reference: str) -> dict:
        """触发镜像安全扫描"""
        resp = self.session.post(
            f"{self.base_url}/api/v2.0/projects/{project}"
            f"/repositories/{repo}/artifacts/{reference}/scan"
        )
        if resp.status_code in (200, 201):
            return {"status": "scan_started", "reference": reference}
        return {"status": "error", "detail": resp.text}


# 使用示例
if __name__ == "__main__":
    harbor = HarborManager(
        url="https://harbor.yourdomain.com",
        username="admin",
        password="Harbor12345"
    )

    # 列出所有项目
    print("=== 项目列表 ===")
    projects = harbor.list_projects()
    for p in projects:
        print(f"  📁 {p['name']} ({p['repo_count']} 个仓库)")

    # 清理旧镜像
    print("\n=== 清理旧镜像 ===")
    for project in ["myapp"]:
        repos = harbor.list_repositories(project)
        for repo in repos:
            results = harbor.cleanup_old_images(project, repo, keep_tags=3)
            for r in results:
                print(f"  {r}")

    # 存储配额
    print("\n=== 存储配额 ===")
    for project in ["myapp"]:
        usage = harbor.get_quota_usage(project)
        print(f"  {project}: {usage['used'] / 1024**3:.1f}GB / "
              f"{usage['hard'] / 1024**3:.1f}GB "
              f"({usage['usage_percent']}%)")

3. 方案二:云厂商托管 ACR(适合快速上手)

以腾讯云 TCR(容器镜像服务)为例,展示云托管方案的使用方式:

# tcr_manager.py — 腾讯云 TCR 镜像仓库管理
import hashlib
import hmac
import time
import base64
import json
import requests
from datetime import datetime

class TCRManager:
    """腾讯云容器镜像服务管理"""

    def __init__(self, secret_id: str, secret_key: str,
                 region: str = "ap-guangzhou"):
        self.secret_id = secret_id
        self.secret_key = secret_key
        self.region = region
        self.base_url = f"https://tcr.tencentcloudapi.com"

    def _sign(self, payload: str) -> str:
        """TC3-HMAC-SHA256 签名"""
        timestamp = int(time.time())
        date = datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d")

        # 拼接签名字符串
        credential_scope = f"{date}/tcr/tc3_request"
        canonical_headers = (
            f"content-type:application/json\n"
            f"host:tcr.tencentcloudapi.com\n"
            f"x-tc-action:DescribeInstances"
        )
        signed_headers = "content-type;host;x-tc-action"

        hash_payload = hashlib.sha256(payload.encode()).hexdigest()
        canonical_request = (
            f"POST\n/\n\n{canonical_headers}\n"
            f"{signed_headers}\n{hash_payload}"
        )
        hash_request = hashlib.sha256(canonical_request.encode()).hexdigest()

        string_to_sign = (
            f"TC3-HMAC-SHA256\n{timestamp}\n"
            f"{credential_scope}\n{hash_request}"
        )

        # 签名
        key_date = hmac.new(
            ("TC3" + self.secret_key).encode(),
            date.encode(), hashlib.sha256
        ).digest()
        key_service = hmac.new(
            key_date, "tcr".encode(), hashlib.sha256
        ).digest()
        key_signing = hmac.new(
            key_service, "tc3_request".encode(), hashlib.sha256
        ).digest()
        signature = hmac.new(
            key_signing, string_to_sign.encode(), hashlib.sha256
        ).hexdigest()

        return {
            "Authorization": (
                f"TC3-HMAC-SHA256 Credential={self.secret_id}/"
                f"{credential_scope}, SignedHeaders={signed_headers}, "
                f"Signature={signature}"
            ),
            "Content-Type": "application/json",
            "X-TC-Action": "DescribeInstances",
            "X-TC-Version": "2022-05-12",
            "X-TC-Region": self.region,
            "X-TC-Timestamp": str(timestamp),
        }

    def list_instances(self) -> list:
        """列出镜像仓库实例"""
        payload = json.dumps({})
        headers = self._sign(payload)
        resp = requests.post(self.base_url, headers=headers, data=payload)
        if resp.status_code == 200:
            data = resp.json()
            return data.get("Response", {}).get("Instances", [])
        print(f"请求失败: {resp.status_code} {resp.text}")
        return []


def docker_login_tcr():
    """Docker 登录腾讯云 TCR"""
    # 格式:docker login ccr.ccs.tencentyun.com
    import subprocess
    cmd = (
        "echo 'your_password' | "
        "docker login ccr.ccs.tencentyun.com "
        "-u your_appid"
    )
    print(f"登录命令: docker login ccr.ccs.tencentyun.com")
    print("⚠️ 实际使用时请替换为真实密码,不要硬编码")


def push_image_to_tcr():
    """推送镜像到 TCR 完整流程"""
    steps = """
    # 1. 标记镜像
    docker tag myapp:v1.0 ccr.ccs.tencentyun.com/your-ns/myapp:v1.0

    # 2. 推送镜像
    docker push ccr.ccs.tencentyun.com/your-ns/myapp:v1.0

    # 3. 验证
    docker pull ccr.ccs.tencentyun.com/your-ns/myapp:v1.0
    """
    print(steps.strip())


if __name__ == "__main__":
    # 云方案的使用非常简单——主要是 docker login + push
    print("=== 云厂商 ACR 方案 ===")
    print("优势:零运维、全球加速、自动漏洞扫描")
    print("步骤:")
    print("  1. 控制台开通 TCR 服务")
    print("  2. docker login <镜像仓库地址>")
    print("  3. docker push <镜像仓库地址>/namespace/image:tag")
    print("  4. 在控制台配置自动扫描 + 生命周期规则")
    print()
    push_image_to_tcr()

4. 方案三:GHCR(适合开源项目和 GitHub 生态)

GitHub Container Registry 与 GitHub Actions 深度集成,对开源项目最友好:

# .github/workflows/build-and-push.yml — GHCR 自动构建推送
name: Build and Push Docker Image

on:
  push:
    branches: [main]
    tags: ["v*"]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
# ghcr_analyzer.py — GHCR 使用分析脚本
import requests
from typing import Optional

class GHCRAnalyzer:
    """GitHub Container Registry 分析工具"""

    def __init__(self, token: str = ""):
        self.base_url = "https://api.github.com"
        self.headers = {
            "Accept": "application/vnd.github.v3+json",
        }
        if token:
            self.headers["Authorization"] = f"Bearer {token}"

    def list_packages(self, org: str) -> list:
        """列出组织的所有容器包"""
        resp = requests.get(
            f"{self.base_url}/orgs/{org}/packages",
            params={"package_type": "container", "per_page": 100},
            headers=self.headers
        )
        resp.raise_for_status()
        return [
            {
                "name": p["name"],
                "visibility": p["visibility"],
                "created_at": p["created_at"][:10],
                "updated_at": p["updated_at"][:10],
            }
            for p in resp.json()
        ]

    def get_package_versions(self, org: str, package: str) -> list:
        """获取包的所有版本"""
        resp = requests.get(
            f"{self.base_url}/orgs/{org}/packages/container/{package}/versions",
            params={"per_page": 10},
            headers=self.headers
        )
        resp.raise_for_status()
        versions = []
        for v in resp.json():
            tags = [m["name"] for m in v.get("metadata", {}).get("container", {}).get("tags", [])]
            versions.append({
                "id": v["id"],
                "tags": tags,
                "created": v["created_at"][:10],
                "size_mb": round(v.get("size", 0) / 1024 / 1024, 1),
            })
        return versions

    def compare_storage(self, org: str):
        """对比存储使用情况"""
        packages = self.list_packages(org)
        total_size = 0
        print(f"=== {org} 镜像仓库概况 ===")
        print(f"{'名称':<30} {'可见性':<10} {'最后更新':<12}")
        print("-" * 55)

        for pkg in packages:
            print(f"{pkg['name']:<30} {pkg['visibility']:<10} {pkg['updated_at']:<12}")

        print(f"\n总计: {len(packages)} 个包")
        print("⚠️ GHCR 不提供精确存储总量,建议在 GitHub Settings > Billing 查看")


if __name__ == "__main__":
    # 使用方式
    analyzer = GHCRAnalyzer()
    # analyzer.list_packages("your-org")
    print("GHCR 优势:与 GitHub Actions 无缝集成")
    print("GHCR 限制:无多区域同步,私有仓库有流量费用")

5. 选型决策矩阵

根据你的实际场景选择:

场景 推荐方案 原因
10 人以下小团队,用 GitHub GHCR 零配置,CI/CD 原生支持
中型企业,需要合规审计 Harbor 完整 RBAC + 扫描 + 签名
已在用阿里云/腾讯云 云 ACR 与现有云服务深度集成
需要跨云/跨区域同步 Harbor 或云 ACR 都支持 replication
开源项目维护 GHCR 公开仓库免费 + 依赖扫描
金融/政务合规场景 Harbor(私有化部署) 数据不出内网

成本对比(存储 100GB / 月传输 500GB)

方案 存储 传输 合计 隐藏成本
Harbor 自建 服务器 ¥300/月 集群内网免费 ~¥300 运维人力 0.5 人
腾讯云 TCR ¥15 ¥50 ~¥65 无
阿里云 ACR ¥12 ¥60 ~¥72 无
GHCR $0(公开) $0 $0 私有仓库 $0.02/GB/天

6. 实用技巧:统一管理多仓库

当你同时使用多个镜像仓库时,可以用 regctl 统一管理:

# 安装 regctl
go install github.com/regclient/regclient/cmd/regctl@latest

# 配置多仓库
export REGCTL_CONFIG=~/.regctl/config.yaml

# 从 Harbor 同步到 TCR
regctl image copy \
  harbor.example.com/myapp:v1.0 \
  ccr.ccs.tencentyun.com/ns/myapp:v1.0

# 从 GHCR 拉取到本地
regctl image pull ghcr.io/username/myapp:latest

# 查看镜像详情(layer、size、digest)
regctl manifest get ghcr.io/username/myapp:latest

# 批量打标签(为同一镜像打不同 tag)
for tag in v1.0 v1.0.0 latest; do
  regctl image tag harbor.example.com/myapp:v1.0 \
    harbor.example.com/myapp:$tag
done

一句话总结:如果你已经在用某家云厂商的 K8s 服务,优先用它的 ACR;如果你需要数据不出内网或者有复杂的合规要求,选 Harbor;如果你是开源项目维护者,GHCR 最省心。


👤 作者简介

一枚在大中原腹地(河南)卖公有云的女/男士,主营腾讯云/阿里云/华为云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「AI热点日报」,围观AI前沿动态~

博客:y1cloud.cn

发表评论