Skopeo

Skopeo Skopeo 用于检查、复制和同步容器镜像,支持远程 Registry、Docker 归档和 OCI 镜像布局等存储格式。仓库之间复制镜像通常不需要 Docker daemon,也不需要 root 权限;访问 Docker daemon 或受限的本地镜像存储时另有权限要求。 本文示例以

Skopeo

Skopeo 用于检查、复制和同步容器镜像,支持远程 Registry、Docker 归档和 OCI 镜像布局等存储格式。仓库之间复制镜像通常不需要 Docker daemon,也不需要 root 权限;访问 Docker daemon 或受限的本地镜像存储时另有权限要求。

本文示例以 Linux Bash 为环境,基础参数对照 Ubuntu 22.04 提供的 Skopeo 1.4.1;较新版本的可选功能单独注明。仓库域名、项目、镜像标签均为示例,使用前替换并确认目标项目存在、账号有相应权限。另一款工具已拆分到 Image-Syncer。

1. 安装与使用边界

Debian / Ubuntu:

sudo apt-get update
sudo apt-get install -y skopeo jq
skopeo --version
skopeo copy --help
skopeo sync --help

Fedora / RHEL 系列按发行版仓库安装:

sudo dnf install -y skopeo jq

发行版可能提供较旧版本,不能直接把主分支文档中的所有参数用于旧包。安装和升级参考官方安装说明。

传输格式 示例 用途与限制
docker:// docker://registry.example.com/team/app:v1 Registry HTTP API V2 镜像;不表示需要本机 Docker
docker-archive: docker-archive:./app.tar:team/app:v1 Docker save 格式归档;写入时可指定归档中的镜像名称与标签
oci: oci:./app-oci:v1 OCI Image Layout 目录,适合保留多架构索引
oci-archive: oci-archive:./app-oci.tar:v1 OCI 格式的 tar 归档;接收端须支持此格式
dir: dir:./app-dir Skopeo 的 manifest、层和签名目录;不是 OCI 布局,也不是 docker load 输入
docker-daemon: docker-daemon:team/app:v1 Docker daemon 本地存储;需要可用的 daemon 及访问权限
containers-storage: containers-storage:team/app:v1 Podman / Buildah / CRI-O 使用的 containers/storage;不是 containerd 镜像存储

copy 使用上表中的完整传输前缀。sync 则使用 --src、--dest 分别指定传输类型,位置参数不加 docker://;它支持的源为 docker、dir、yaml,目标为 docker、dir。

常用全局参数放在子命令前,例如 skopeo --command-timeout=30m copy ...。--override-os、--override-arch、--override-variant 用于选择已有的平台镜像,不会转换二进制架构。

2. 认证、证书与信任策略

2.1 显式保存认证信息

交互式登录时由命令提示输入密码,避免把密码直接写入命令历史。以下文件会被后续命令复用:

install -d -m 700 "$HOME/.config/containers"
export REGISTRY_AUTH_FILE="$HOME/.config/containers/migration-auth.json"
umask 077
skopeo login --authfile "$REGISTRY_AUTH_FILE" \
  --username source-user harbor.example.com
skopeo login --authfile "$REGISTRY_AUTH_FILE" \
  --username target-user registry.example.com
chmod 600 "$REGISTRY_AUTH_FILE"

自动化任务可通过密钥管理系统注入密码,再使用 login --password-stdin。认证文件中的 auth 通常为 Base64 编码,不能当作加密存储。不要提交到文档或代码仓库。默认认证路径可能位于登录会话的运行目录,定时任务使用显式路径更容易管理。

同一账号需要在源仓库具备 pull 权限,在目标仓库具备 push 权限;列举全部标签还需要能访问标签列表。Harbor 等平台通常需要预先创建目标项目。以其他用户或 sudo 执行时,注意认证文件路径、所有者和读取权限。

2.2 私有 CA 与 TLS

可在 /etc/containers/certs.d/<仓库主机[:端口]>/ca.crt 配置信任的 CA,或在复制命令中为两端显式指定证书目录:

skopeo copy \
  --src-cert-dir /path/to/source-certs \
  --dest-cert-dir /path/to/target-certs \
  docker://harbor.example.com/team/app:v1 \
  docker://registry.example.com/mirror/app:v1

证书目录中的 CA 文件使用 .crt 后缀;需要双向 TLS 时另配置客户端 .cert、.key。目录应对应实际仓库地址和端口。

--src-tls-verify=false、--dest-tls-verify=false 会降低对应端的 HTTPS/TLS 保护,只用于明确使用 HTTP 或临时排障的环境。inspect、login 使用 --tls-verify,参数名不能混用。

--insecure-policy 关闭镜像信任策略限制,和关闭 TLS 验证是两件事。遇到 policy.json 缺失应先检查发行版提供的 containers 配置包及组织签名策略,不应把这个开关作为默认参数。

3. 查看镜像与平台信息

# list-tags 的参数为仓库路径,不带 :tag
skopeo list-tags docker://harbor.example.com/team/app

skopeo inspect docker://harbor.example.com/team/app:v1
skopeo inspect --config docker://harbor.example.com/team/app:v1

# 获取原始 manifest / index,不下载完整文件系统层
skopeo inspect --raw docker://harbor.example.com/team/app:v1 > source-manifest.json
jq '{mediaType, platforms: [.manifests[]? | .platform]}' source-manifest.json
skopeo manifest-digest source-manifest.json

普通 inspect 组合了镜像摘要和所选平台的配置信息;不能仅凭其中的 Architecture 判断一个标签是否包含多个平台。--raw 返回的 manifests 数组可用于检查 manifest list / OCI index;单平台 manifest 没有这个数组。inspect 通常只获取 manifest、配置及必要的仓库元数据,不会拉取全部镜像层。

指定平台查看:

skopeo --override-os=linux --override-arch=arm64 inspect \
  docker://harbor.example.com/team/app:v1

若目标平台不存在,这个参数不会生成它。arm/v7 等平台还需根据实际索引设置 --override-arch=arm --override-variant=v7。

4. 复制镜像

4.1 仓库到仓库,保留全部平台

skopeo --command-timeout=30m copy --all --retry-times=3 \
  --digestfile=./app-v1.digest \
  docker://harbor.example.com/team/app:v1 \
  docker://registry.example.com/mirror/app:v1

--all 复制 manifest list / index 及其引用的全部镜像;不加时通常只复制当前 OS / CPU 平台。单平台镜像也可使用此参数。复制完成后按第 6 节比较摘要。

--digestfile 在成功复制后记录目标镜像摘要,便于保存迁移结果;它不会替代源、目标摘要比较。

较新版本的 copy 支持 --preserve-digests,可在上述命令中加入该参数,要求保留摘要;无法保留时直接失败。Ubuntu 22.04 的 1.4.1 没有这个参数,必须先检查 skopeo copy --help。若迁移流程要求强制保留摘要,应升级到支持该参数的版本,并在执行后再次校验。

标签可被覆盖,长期迁移计划应记录源摘要,关键镜像使用 docker://harbor.example.com/team/app@sha256:<实际摘要> 固定来源。占位摘要不能直接执行。固定的是顶层索引摘要时,仍需 --all 才能复制所有平台。

仅迁移 Linux amd64 时明确选择平台,并省略 --all:

skopeo --override-os=linux --override-arch=amd64 copy --retry-times=3 \
  docker://harbor.example.com/team/app:v1 \
  docker://registry.example.com/mirror/app:v1-amd64

这会产生单平台目标,目标顶层摘要通常与源多架构索引不同。不要再用顶层摘要相等作为该操作的验收条件。

4.2 本机 Docker 镜像与归档导入

镜像已存在于本机 Docker daemon 时,可以直接复制到 Registry,不必先执行 docker save。此方式需要 Docker daemon 正在运行,当前用户也必须有权访问它:

skopeo copy docker-daemon:team/app:v1 \
  docker://registry.example.com/mirror/app:v1

如果要离线传输或接收端需要 Docker save 格式,再使用归档流程:

skopeo --override-os=linux --override-arch=amd64 copy \
  docker://harbor.example.com/team/app:v1 \
  docker-archive:./app-v1.tar:harbor.example.com/team/app:v1

# 有 Docker 的接收端
docker load -i ./app-v1.tar

# 或直接推回 Registry,不经过 Docker daemon
skopeo copy docker-archive:./app-v1.tar \
  docker://registry.example.com/mirror/app:v1

docker-archive: 写出 Docker save 格式的 tar;文件名写成 .tar.gz 不会自动启用 gzip。归档中的名称与标签在导出时显式设置,避免导入后没有预期名称。此处是单平台流程,Docker archive 目标不用于保存完整多架构索引。

在 containerd / Kubernetes 节点上使用 nerdctl 时,可导入同一归档:

sudo nerdctl --namespace k8s.io load -i ./app-v1.tar
sudo nerdctl --namespace k8s.io images

k8s.io 是 Kubernetes CRI 通常使用的 containerd namespace;普通 nerdctl 默认 namespace 不同。导入不等于在 Kubernetes 中创建 Pod,还要检查 Pod 的镜像名、imagePullPolicy、节点平台及实际调度节点。

4.3 多架构离线迁移:使用 OCI 布局

联网端导出并为传输包记录校验值:

skopeo copy --all \
  docker://harbor.example.com/team/app:v1 oci:./app-oci:v1
tar -C . -cf app-oci.tar app-oci
sha256sum app-oci.tar > app-oci.tar.sha256

将 app-oci.tar 和校验文件送至离线端,在专用目录校验、解包,再上传离线仓库:

sha256sum -c app-oci.tar.sha256
tar -xf app-oci.tar
skopeo copy --all oci:./app-oci:v1 \
  docker://registry.example.com/mirror/app:v1

OCI 与 Docker media type 的转换可能改变摘要;要求摘要不变时,在支持的版本中为两次 copy 都加上 --preserve-digests,失败后评估兼容性。传输包校验值验证文件传输完整性,镜像 manifest 摘要验证镜像内容身份,二者用途不同。

5. 批量同步

5.1 YAML 明确来源和标签

将下面内容保存为 skopeo-sync.yaml,镜像路径相对于顶层仓库地址,不带 docker://:

harbor.example.com:
  tls-verify: true
  images:
    team/app:
      - "v1"
      - "v2"
    team/nginx:
      - "1.26.3"
  images-by-tag-regex:
    team/stable: '^v[0-9]+\.[0-9]+\.[0-9]+$'

条目值为 [] 时表示同步该镜像仓库的全部标签,可能涉及大量历史版本;它不是“跳过该镜像”。images-by-tag-regex 可按标签名筛选,例如上例只匹配 team/stable 中完整的 v主版本.次版本.修订号 标签,不匹配带后缀的预发布版本。sync 只处理已指定的镜像仓库,不会自动发现整个 Registry 的所有项目或仓库;筛选字段以所用版本的手册为准。

skopeo --command-timeout=1h sync --all --retry-times=3 \
  --src yaml --dest docker \
  ./skopeo-sync.yaml registry.example.com/mirror

Skopeo sync YAML 不会自动展开 ${PASSWORD}。不要把密码直接写在同步清单中;复用前面登录生成的认证文件。REGISTRY_AUTH_FILE 可供基础命令使用,sync 也可显式设置 --authfile,或用 --src-authfile / --dest-authfile 分开认证。

5.2 目标名称与冲突

默认不加 --scoped 时,只取源镜像路径的最后一段作为目标名称:

源镜像 目标前缀 最终目标
harbor.example.com/team/app:v1 registry.example.com/mirror registry.example.com/mirror/app:v1
harbor.example.com/team/nginx:1.26.3 registry.example.com/mirror registry.example.com/mirror/nginx:1.26.3

多个来源的 app:v1 会落到同一个目标,可能互相覆盖。需要保留来源路径时:

skopeo sync --all --scoped --retry-times=3 \
  --src yaml --dest docker \
  ./skopeo-sync.yaml registry.example.com/mirror

这时示例目标为 registry.example.com/mirror/harbor.example.com/team/app:v1。先确认目标仓库允许这样的多级路径、仓库名和权限范围。源地址带 :端口 时,某些版本会把端口中的冒号直接拼入目标仓库路径并报 invalid reference format,不要把 --scoped 当作任意来源通用的防冲突方案。需要自定义路径或替换来源主机、端口时,使用多条 copy 或 Image-Syncer。

较新版本提供 sync --dry-run,可预览映射;Ubuntu 22.04 的 1.4.1 不提供该参数。先检查 skopeo sync --help,支持时再执行:

skopeo sync --dry-run --all --scoped \
  --src yaml --dest docker \
  ./skopeo-sync.yaml registry.example.com/mirror

预览仍可能访问源仓库以列举标签;它不能证明目标有推送权限、空间充足或接受相应镜像格式。旧版本先迁移一个测试标签并核对名称,再扩大范围。

5.3 同步语义与资源消耗

sync 是一次性复制任务,不是持续运行的控制器,也不会默认删除目标中多余的标签。目标标签可被源标签覆盖;是否允许覆盖取决于仓库不可变标签策略。源仓库中已存在的层在目标也存在时通常可复用,日志中的 skipped: already exists 不等于整个迁移已验收。

仓库间复制通常按流传输,仍需要网络、内存和 CPU。转换、压缩、预计算摘要或某些后端可能使用临时文件;不能保证没有磁盘 I/O。使用 dir、oci、归档目标时会明确写入磁盘。必要时用全局 --tmpdir=/path/to/tmp 指定空间充足的临时目录,并按目标配置检查容量。

中转主机通常同时接收源流量并向目标发送,但速率不必相等:已存在的层、跨仓库挂载、缓存、重试、压缩和网络瓶颈都会改变观测结果。不要仅用网卡速率判断同步成功。定时迁移需由 systemd timer、cron 或 CI 调度,并保留退出码与日志。

6. 验收、签名和删除边界

6.1 比较顶层摘要

完整迁移并保留原 manifest 时:

set -euo pipefail
skopeo inspect --raw docker://harbor.example.com/team/app:v1 > source.json
skopeo inspect --raw docker://registry.example.com/mirror/app:v1 > target.json
source_digest=$(skopeo manifest-digest source.json)
target_digest=$(skopeo manifest-digest target.json)
test "$source_digest" = "$target_digest"
printf '源摘要:%s\n目标摘要:%s\n' "$source_digest" "$target_digest"
jq -r '.manifests[]? | [.platform.os, .platform.architecture, (.platform.variant // ""), .digest] | @tsv' target.json

摘要相等说明顶层 manifest 字节一致。对于多架构镜像,还应确认每个预期平台的子 manifest 和层均可读取;仅有索引不代表所有内容都上传成功。单平台提取、格式转换或重压缩时,摘要可以合理变化,需改为核对所选平台的配置和层,并在对应平台实际拉取、运行验收。逐标签迁移清单应记录源、目标、摘要和成功状态。

镜像可启动也不代表签名、SBOM、漏洞报告或 OCI referrers 已全部迁移。Skopeo 的签名处理依赖格式、仓库能力和签名配置;copy --all 指的是平台镜像,不承诺复制所有供应链关联产物。按使用的签名系统另行迁移并验证。--remove-signatures 会移除签名,只在明确接受该结果时使用。

6.2 删除不是普通标签清理

skopeo delete docker://registry.example.com/mirror/app:old 在某些 Registry 中会先将标签解析为 manifest 摘要再删除 manifest,可能影响指向同一摘要的其他标签。支持情况和权限依赖仓库实现。仅删除一个标签时优先使用仓库自身的标签管理接口,并先确认影响范围;本次文档检查没有执行删除操作。

删除 manifest 通常不会立刻释放所有层占用;空间回收由 Registry / Harbor 的垃圾回收机制处理,应遵循对应产品的维护流程。

7. 常见故障

现象 重点检查
unauthorized / denied 当前执行用户、认证文件、令牌有效期、源 pull / 目标 push 权限、目标项目是否存在
x509: certificate signed by unknown authority 私有 CA、证书域名、完整证书链及仓库端口对应的证书目录
manifest unknown / name unknown 镜像路径、大小写、标签是否存在;仓库是否隐藏未授权资源
no image found in manifest list 源是否包含指定 OS / 架构 / variant;--override-* 不能转换架构
多架构变成单平台 是否缺少 --all,是否误用单平台 Docker archive 流程
unknown flag: --dry-run / --preserve-digests 旧版本不支持;核对对应子命令帮助或升级版本
--scoped 后 invalid reference format 源仓库是否带端口,完整路径是否满足目标仓库的命名限制;改为显式映射
目标摘要变化 / --preserve-digests 失败 是否进行格式转换、层重压缩或平台筛选;目标仓库支持情况
429 / 超时 仓库限流、网络、代理、重试和超时配置;避免盲目提高并发
no space left on device 临时目录、归档目录、inode 和目标 Registry 存储容量
导入后 Pod 仍然拉取失败 containerd namespace、实际节点、镜像名和标签、平台、imagePullPolicy

8. 原有示意图与操作截图

以下保留原实验的图片,仓库地址、版本及输出仅代表历史环境;可执行命令和参数以本文章节为准。

仓库复制及整体迁移流程:

仓库间复制镜像的操作截图

目标仓库中的镜像记录

通过中转主机同步镜像的流程图

批量同步与层复用:

批量镜像同步日志

目标仓库已有镜像层的复用日志

同步过程中中转主机带宽观测

同步完成与目标检查:

一组同步任务的完成记录

另一组多标签任务的完成记录

目标镜像仓库的历史验证截图

9. 参考资料

评论