Tools: Latest: Stop Pulling Containers Just to Mirror Them: Practical `skopeo` for Safer Image Promotion

Tools: Latest: Stop Pulling Containers Just to Mirror Them: Practical `skopeo` for Safer Image Promotion

Why skopeo is worth keeping around

Install skopeo

1) Inspect a remote image without pulling it

2) List available tags before choosing one

3) Pin by digest, not by mutable tag

4) Export an image as a Docker-compatible archive

5) Build a small offline mirror with skopeo sync

6) Copy directly from registry to registry

7) Understand where credentials live

Important gotchas

Multi-arch images are special

dir: is convenient, but it's not the OCI layout

Avoid --tls-verify=false unless this is a throwaway lab

A practical pattern I like

Final takeaway If your workflow for moving container images still starts with docker pull, you've probably accepted more friction than you need. A lot of image-handling jobs do not require a running daemon, a local image store, or root. Sometimes you just want to: That is exactly where skopeo shines. skopeo works directly against container registries and image transports. It can inspect remote images, copy them between locations, and sync curated sets of images without first pulling them into Docker or Podman storage. In this post, I'll show a practical workflow you can reuse on Linux. According to the upstream project and the skopeo(1) man page, skopeo: That makes it a great fit for: If your distro doesn't package it by default, check the upstream install notes for supported package sources. Let's inspect Alpine directly from Docker Hub: Useful fields to look at: If you only want the digest: A common mistake is hard-coding latest and hoping for the best. That lets you choose a real published tag instead of guessing. Tags can move. Digests are the safer promotion boundary. Now copy the exact image by digest into an OCI layout: If another system expects docker load, export a docker-archive: Inspect the saved archive's tags: This is handy when you need to: For air-gapped or tightly controlled environments, skopeo sync is the practical workhorse. Create a YAML file that defines exactly what you want mirrored: If the plan looks right, run it for real: This pattern is much safer than mirroring an entire repo blindly. When you need promotion instead of local export, copy directly: For private registries, authenticate first: Then inspect the promoted result: A useful habit here is comparing the source and destination digests after the copy. Container tools that use the containers/image stack typically use an auth file at: Per containers-auth.json(5), tools may also fall back to: That matters because skopeo, podman, and other related tools can often share registry credentials rather than forcing you to log in repeatedly. Per skopeo-copy(1) and skopeo-sync(1), if the source is a multi-architecture image, the default behavior is typically to copy only the image matching the current system architecture. If you want the full multi-arch image list, use: dir: is useful for debugging and non-invasive inspection, but it's a non-standardized local directory format. If you want a standards-based on-disk layout, prefer oci:. If a registry certificate is wrong, fix trust properly instead of normalizing insecure flags into production scripts. For CI or controlled promotion pipelines, this sequence is hard to beat: That gives you a workflow that is more reproducible, more reviewable, and less dependent on heavyweight local runtime state. If you mostly use container tools from the runtime side, skopeo can feel easy to overlook. But for inspection, promotion, export, and mirroring, it's one of the cleanest tools in the Linux container stack. You do not need to pull everything locally just to answer basic questions or move an image safely from one place to another. Sometimes the best container workflow is the one that never starts a daemon in the first place. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y skopeo jq -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y skopeo jq -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y skopeo jq skopeo --version skopeo --version skopeo --version skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq '{Name, Digest, Created, Architecture, Os, Layers}' skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq '{Name, Digest, Created, Architecture, Os, Layers}' skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq '{Name, Digest, Created, Architecture, Os, Layers}' skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq -r '.Digest' skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq -r '.Digest' skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq -r '.Digest' skopeo list-tags -weight: 500;">docker://-weight: 500;">docker.io/library/alpine | jq '.Tags[:20]' skopeo list-tags -weight: 500;">docker://-weight: 500;">docker.io/library/alpine | jq '.Tags[:20]' skopeo list-tags -weight: 500;">docker://-weight: 500;">docker.io/library/alpine | jq '.Tags[:20]' DIGEST=$(skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq -r '.Digest') printf '%s\n' "$DIGEST" DIGEST=$(skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq -r '.Digest') printf '%s\n' "$DIGEST" DIGEST=$(skopeo inspect -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 | jq -r '.Digest') printf '%s\n' "$DIGEST" mkdir -p ./mirror/alpine skopeo copy \ --preserve-digests \ "-weight: 500;">docker://-weight: 500;">docker.io/library/alpine@${DIGEST}" \ oci:./mirror/alpine:3.20 mkdir -p ./mirror/alpine skopeo copy \ --preserve-digests \ "-weight: 500;">docker://-weight: 500;">docker.io/library/alpine@${DIGEST}" \ oci:./mirror/alpine:3.20 mkdir -p ./mirror/alpine skopeo copy \ --preserve-digests \ "-weight: 500;">docker://-weight: 500;">docker.io/library/alpine@${DIGEST}" \ oci:./mirror/alpine:3.20 find ./mirror/alpine -maxdepth 2 -type f | sort find ./mirror/alpine -maxdepth 2 -type f | sort find ./mirror/alpine -maxdepth 2 -type f | sort mkdir -p ./archives skopeo copy \ "-weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20" \ -weight: 500;">docker-archive:./archives/alpine-3.20.tar:-weight: 500;">docker.io/library/alpine:3.20 mkdir -p ./archives skopeo copy \ "-weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20" \ -weight: 500;">docker-archive:./archives/alpine-3.20.tar:-weight: 500;">docker.io/library/alpine:3.20 mkdir -p ./archives skopeo copy \ "-weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20" \ -weight: 500;">docker-archive:./archives/alpine-3.20.tar:-weight: 500;">docker.io/library/alpine:3.20 skopeo list-tags -weight: 500;">docker-archive:./archives/alpine-3.20.tar | jq skopeo list-tags -weight: 500;">docker-archive:./archives/alpine-3.20.tar | jq skopeo list-tags -weight: 500;">docker-archive:./archives/alpine-3.20.tar | jq # sync.yml -weight: 500;">docker.io: images: library/alpine: - "3.20" library/busybox: - "1.36" quay.io: images: libpod/alpine: - "latest" # sync.yml -weight: 500;">docker.io: images: library/alpine: - "3.20" library/busybox: - "1.36" quay.io: images: libpod/alpine: - "latest" # sync.yml -weight: 500;">docker.io: images: library/alpine: - "3.20" library/busybox: - "1.36" quay.io: images: libpod/alpine: - "latest" mkdir -p /tmp/skopeo-mirror skopeo sync --dry-run --src yaml --dest dir sync.yml /tmp/skopeo-mirror mkdir -p /tmp/skopeo-mirror skopeo sync --dry-run --src yaml --dest dir sync.yml /tmp/skopeo-mirror mkdir -p /tmp/skopeo-mirror skopeo sync --dry-run --src yaml --dest dir sync.yml /tmp/skopeo-mirror skopeo sync --src yaml --dest dir sync.yml /tmp/skopeo-mirror skopeo sync --src yaml --dest dir sync.yml /tmp/skopeo-mirror skopeo sync --src yaml --dest dir sync.yml /tmp/skopeo-mirror find /tmp/skopeo-mirror -maxdepth 3 -type f | sort find /tmp/skopeo-mirror -maxdepth 3 -type f | sort find /tmp/skopeo-mirror -maxdepth 3 -type f | sort skopeo copy \ --preserve-digests \ -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 \ -weight: 500;">docker://registry.example.com/base/alpine:3.20 skopeo copy \ --preserve-digests \ -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 \ -weight: 500;">docker://registry.example.com/base/alpine:3.20 skopeo copy \ --preserve-digests \ -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 \ -weight: 500;">docker://registry.example.com/base/alpine:3.20 skopeo login registry.example.com skopeo login registry.example.com skopeo login registry.example.com skopeo inspect -weight: 500;">docker://registry.example.com/base/alpine:3.20 | jq '{Name, Digest}' skopeo inspect -weight: 500;">docker://registry.example.com/base/alpine:3.20 | jq '{Name, Digest}' skopeo inspect -weight: 500;">docker://registry.example.com/base/alpine:3.20 | jq '{Name, Digest}' ${XDG_RUNTIME_DIR}/containers/auth.json ${XDG_RUNTIME_DIR}/containers/auth.json ${XDG_RUNTIME_DIR}/containers/auth.json skopeo copy --all -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 oci:./mirror/alpine-all:3.20 skopeo copy --all -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 oci:./mirror/alpine-all:3.20 skopeo copy --all -weight: 500;">docker://-weight: 500;">docker.io/library/alpine:3.20 oci:./mirror/alpine-all:3.20 - inspect an image before trusting it - pin the exact digest your CI should promote - copy an image into an OCI layout or a -weight: 500;">docker-archive - mirror a small approved set of images for a disconnected environment - works with remote registries and OCI/Docker image formats - does not require a daemon for most operations - usually does not require root unless you target a runtime storage backend - can inspect remote images without fully pulling them first - CI pipelines that need to validate or promote images - bastion or utility hosts that should stay lean - air-gapped preparation workflows - safer image promotion where you want digest-based control - you can confirm the registry path and digest before promotion - you can inspect labels and metadata without populating local image storage - you can use the digest for reproducible downstream steps - an OCI image layout on disk - a workflow tied to the exact content you inspected - less risk that a tag changes between validation and promotion - hand off an image file between environments - preload images onto systems without direct registry access - feed a controlled artifact into another stage - a reviewable allowlist of images and tags - a repeatable sync definition you can commit to Git - a clean boundary for disconnected or regulated environments - ~/.config/containers/auth.json - ~/.-weight: 500;">docker/config.json - ~/.dockercfg - skopeo inspect the candidate image - record the digest - copy by digest, not by tag - verify the destination digest - sync only approved images through a YAML allowlist when building mirrors - Skopeo upstream project: https://github.com/containers/skopeo - skopeo(1) man page: https://manpages.ubuntu.com/manpages/noble/man1/skopeo.1.html - skopeo-copy(1) man page: https://manpages.ubuntu.com/manpages/noble/man1/skopeo-copy.1.html - skopeo-sync(1) man page: https://manpages.ubuntu.com/manpages/noble/man1/skopeo-sync.1.html - skopeo-list-tags(1) man page: https://manpages.ubuntu.com/manpages/noble/man1/skopeo-list-tags.1.html - containers-transports(5) man page: https://manpages.ubuntu.com/manpages/noble/man5/containers-transports.5.html - containers-auth.json(5) man page: https://manpages.ubuntu.com/manpages/noble/man5/containers-auth.json.5.html - Cover image: Wikimedia Commons, Utah Data Center panorama: https://commons.wikimedia.org/wiki/File:Utah_Data_Center_Panorama_(cropped).jpg