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. 原有示意图与操作截图
以下保留原实验的图片,仓库地址、版本及输出仅代表历史环境;可执行命令和参数以本文章节为准。
仓库复制及整体迁移流程:



批量同步与层复用:



同步完成与目标检查:



评论