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:
- inspect an image before trusting it
- pin the exact digest your CI should promote
- copy an image into an OCI layout or a
docker-archive - mirror a small approved set of images for a disconnected environment
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.
Why skopeo is worth keeping around
According to the upstream project and the skopeo(1) man page, skopeo:
- 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
That makes it a great fit for:
- 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
Install skopeo
On Debian or Ubuntu:
sudo apt update
sudo apt install -y skopeo jq
Verify it:
skopeo --version
If your distro doesn't package it by default, check the upstream install notes for supported package sources.
1) Inspect a remote image without pulling it
Let's inspect Alpine directly from Docker Hub:
skopeo inspect docker://docker.io/library/alpine:3.20 | jq
Useful fields to look at:
skopeo inspect docker://docker.io/library/alpine:3.20 | jq '{Name, Digest, Created, Architecture, Os, Layers}'
Why this matters:
- 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
If you only want the digest:
skopeo inspect docker://docker.io/library/alpine:3.20 | jq -r '.Digest'
2) List available tags before choosing one
A common mistake is hard-coding latest and hoping for the best.
Use list-tags first:
skopeo list-tags docker://docker.io/library/alpine | jq '.Tags[:20]'
That lets you choose a real published tag instead of guessing.
3) Pin by digest, not by mutable tag
Tags can move. Digests are the safer promotion boundary.
Capture the digest:
DIGEST=$(skopeo inspect docker://docker.io/library/alpine:3.20 | jq -r '.Digest')
printf '%s\n' "$DIGEST"
Now copy the exact image by digest into an OCI layout:
mkdir -p ./mirror/alpine
skopeo copy \
--preserve-digests \
"docker://docker.io/library/alpine@${DIGEST}" \
oci:./mirror/alpine:3.20
What you get:
- 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
Quick sanity check:
find ./mirror/alpine -maxdepth 2 -type f | sort
4) Export an image as a Docker-compatible archive
If another system expects docker load, export a docker-archive:
mkdir -p ./archives
skopeo copy \
"docker://docker.io/library/alpine:3.20" \
docker-archive:./archives/alpine-3.20.tar:docker.io/library/alpine:3.20
Inspect the saved archive's tags:
skopeo list-tags docker-archive:./archives/alpine-3.20.tar | jq
This is handy when you need to:
- hand off an image file between environments
- preload images onto systems without direct registry access
- feed a controlled artifact into another stage
5) Build a small offline mirror with skopeo sync
For air-gapped or tightly controlled environments, skopeo sync is the practical workhorse.
Create a YAML file that defines exactly what you want mirrored:
# sync.yml
docker.io:
images:
library/alpine:
- "3.20"
library/busybox:
- "1.36"
quay.io:
images:
libpod/alpine:
- "latest"
Dry-run first:
mkdir -p /tmp/skopeo-mirror
skopeo sync --dry-run --src yaml --dest dir sync.yml /tmp/skopeo-mirror
If the plan looks right, run it for real:
skopeo sync --src yaml --dest dir sync.yml /tmp/skopeo-mirror
Check what landed:
find /tmp/skopeo-mirror -maxdepth 3 -type f | sort
This pattern is much safer than mirroring an entire repo blindly.
It gives you:
- a reviewable allowlist of images and tags
- a repeatable sync definition you can commit to Git
- a clean boundary for disconnected or regulated environments
6) Copy directly from registry to registry
When you need promotion instead of local export, copy directly:
skopeo copy \
--preserve-digests \
docker://docker.io/library/alpine:3.20 \
docker://registry.example.com/base/alpine:3.20
For private registries, authenticate first:
skopeo login registry.example.com
Then inspect the promoted result:
skopeo inspect docker://registry.example.com/base/alpine:3.20 | jq '{Name, Digest}'
A useful habit here is comparing the source and destination digests after the copy.
7) Understand where credentials live
Container tools that use the containers/image stack typically use an auth file at:
${XDG_RUNTIME_DIR}/containers/auth.json
Per containers-auth.json(5), tools may also fall back to:
~/.config/containers/auth.json~/.docker/config.json~/.dockercfg
That matters because skopeo, podman, and other related tools can often share registry credentials rather than forcing you to log in repeatedly.
Important gotchas
Multi-arch images are special
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:
skopeo copy --all docker://docker.io/library/alpine:3.20 oci:./mirror/alpine-all:3.20
dir: is convenient, but it's not the OCI layout
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:.
Avoid --tls-verify=false unless this is a throwaway lab
If a registry certificate is wrong, fix trust properly instead of normalizing insecure flags into production scripts.
A practical pattern I like
For CI or controlled promotion pipelines, this sequence is hard to beat:
-
skopeo inspectthe 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
That gives you a workflow that is more reproducible, more reviewable, and less dependent on heavyweight local runtime state.
Final takeaway
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.
Sources and references
- 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
Top comments (0)