The container-tools
meta-package, found on Fedora Linux and derivatives such as Red Hat Enterprise Linux, is a collection of tools designed to work with Open Container Initiative (OCI) container images. They originated in the containers project on GitHub, which was donated to the Cloud Native Computing Foundation (CNCF) and is now known as the Podman Container Tools project.
The most famous member of the containers
project is Podman. It provides a rootless container engine with a reduced attack surface. Podman also provides advanced capabilities such as integration with systemd and SELinux, quadlets, and pods.
I love Podman—as everyone who works with containers should—but on a typical workday, I use a different member of the containers
project more frequently, particularly when running my containers on a Kubernetes cluster, such as Red Hat OpenShift.
The tool I am talking about is Skopeo, which enables you to inspect and copy container images between container registry servers along with a number of local storage formats.
To demonstrate why Skopeo is so powerful, let me share a few examples of scenarios where Skopeo provides faster and simpler alternatives to tasks you may be used to performing with Podman. Developers used to working with Podman as just a drop-in replacement for the Docker CLI might be amazed by some of those examples.
The commands in this article were tested against a Linux system. They should work unchanged on Windows and macOS, assuming you know how to make required changes for path names and the like. Close to the end of the article, I'll provide some comments on feature parity among the ports of Skopeo for non-Linux operating systems.
Inspecting remote container images
You don't need to download (pull) a container image just to check its metadata. Skopeo can fetch information about an image, such as the manifest, and display its information, without spending the time waiting to download all its image layers:
skopeo inspect docker://quay.io/centos-bootc/centos-bootc:stream10
{
"Name": "quay.io/centos-bootc/centos-bootc",
"Digest": "sha256:42fc456c6eeee3aa999c320febeaf33f62c9f0e50a37acdece90c3b264ac4bc1",
"RepoTags": [ "00be1c9cb099184045ed54ced09433503d886460e8fdf459280eff233922eadd",
...
Skopeo is able to filter and format the metadata, which is nice for scripting, or for helping you visualize only the information you actually need. For example, to check if a container image is actually a bootc container image, you would check for the presence of the containers.bootc
label:
skopeo inspect --format '{{ index .Labels "containers.bootc" }}'
docker://quay.io/centos-bootc/centos-bootc:stream10
1
By convention, all bootc container images should set this property to the value 1
, which you can query by using the --format
flag.
Another example of the skopeo inspect
subcommand enables you to format the output of arrays and maps as JSON, which can then be piped to the jq
tool for easy visualization of the contents:
skopeo inspect --format '{{ json .Labels }}' docker://quay.io/centos-bootc/centos-bootc:stream10 | jq
{
"architecture": "x86_64",
"bootc.diskimage-builder": "quay.io/centos-bootc/bootc-image-builder",
"build-date": "2025-09-12T07:56:38Z",
...
Inspecting local container images
The protocol designator docker://
in previous commands is Skopeo's way of stating that you want it to communicate with a remote container registry server. It could be an older registry that implements only Docker APIs or a newer registry that implements OCI standards. Skopeo and other container tools call this the transport.
Most container tools support a number of different transports, and some of them enable reading or writing container images to local disk using either standard formats (defined by the Open Container Initiative) or proprietary formats (defined by Docker).
For example, this Containerfile builds a trivial (and suboptimal) container image that runs a static website, powered by the Apache HTTP Server.
FROM registry.access.redhat.com/ubi10/ubi:10.0
RUN dnf install -y systemd httpd && \
systemctl enable httpd && \
dnf clean all
RUN sed -i 's/Listen 80/Listen 8080/' /etc/httpd/conf/httpd.conf
RUN echo "Static web site" > /var/www/html/index.html
ENTRYPOINT ["/lib/systemd/systemd"]
Even though I did not explicitly set any labels on my image, it actually inherited a number of labels and other metadata from its parent image, which is the Red Hat Universal Base Image (UBI). If you build the image as httpd-ubi
, you can inspect its labels by using the containers-storage
transport:
skopeo inspect --format '{{ json .Labels }}' containers-storage:localhost/httpd-ubi | jq
In this last example, it would have actually been easier to simply use the podman inspect
command. This illustrates the flexibility of Skopeo to access image metadata from sources outside of a container registry.
Single-command push
When first learning about container tools, most developers are taught to push their container images to a remote registry server using a sequence of two commands:
podman tag localhost/httpd-ubi quay.io/flozanorht/httpd-ubi:latest
podman push quay.io/flozanorht/httpd-ubi:latest
The first command identifies the local image as one intended for the remote registry quay.io and defines the repository and tag it should have there. The second actually copies that image to the remote location specified. Did you know you only need one command to accomplish this?
podman push localhost/httpd-ubi quay.io/flozanorht/httpd-ubi:latest
Or, using Skopeo:
skopeo copy containers-storage:localhost/httpd-ubi docker://quay.io/flozanorht/httpd-ubi:latest
Done this way, any images identified with a registry name, such as quay.io
on my work machine, are images I actually downloaded from a remote registry. I prefer having a clear distinction between images I produced myself and images I downloaded.
Also, consider the fact that at this time Containerfile builds are not reproducible. Building twice from the same Containerfile, RPM packages, and other input files produces different image layers and, consequently a different image manifest and digests. This is because files inside a container image record their last accessed time, which is usually the build time.
Update a floating tag
If my trivial image was built by a continuous integration (CI) system, it is recommended to tag the image with something that resembles a build number or a timestamp. The CI system (for example, Tekton) would perform a task similar to this:
skopeo copy containers-storage:localhost/httpd-ubi docker://quay.io/flozanorht/httpd-ubi:1234
Then, after a number of integration tests, performance tests, and security scans, my image would be promoted to a "release" image and tagged accordingly:
skopeo copy containers-storage:localhost/httpd-ubi docker://quay.io/flozanorht/httpd-ubi:v1.0
Better yet, the CI system would just tag the image on the remote registry, because the locally built image might not be available anymore, or the "promotion" step could be performed by a different machine than the machines which performed build and test steps.
skopeo copy docker://quay.io/flozanorht/httpd-ubi:1234 docker://quay.io/flozanorht/httpd-ubi:v1.0
This second approach, copying from a registry to the same registry to create a new tag, is preferable because it guarantees the images are the same for both tags. When you copy container images from one machine to another, a number of settings (for example, compression) could end up producing different image manifests.
The source and destination registries could even be different registries: one server for use by the CI system, and another to host only release images.
skopeo copy docker://quay.example.com/flozanorht/httpd-ubi:1234 docker://quay.io/flozanorht/httpd-ubi:v1.0
If you wish, also tag that release image as latest:
skopeo copy docker://quay.example.com/flozanorht/httpd-ubi:1234 docker://quay.io/flozanorht/httpd-ubi:latest
In all of these examples, the skopeo copy
command is smart enough to not copy again or overwrite image layers that already exist on the destination registry. If the copy operation is performed only to update a floating tag, the only change that is made is to an image manifest.
Mirror container images to a disconnected container registry
Most medium and large organizations have strict security policies that disallow downloading any software artifact from the internet. Everything must be vetted, not only for compatibility and licensing issues, but also to ensure there was no tampering and no malware is present on the software artifact. Such policies affect container images as well.
There might be dedicated systems that have access to the internet, allowing them to automatically copy container images for internal usage in a similar way to an HTTP proxy cache. But even that might be forbidden, and the expected workflow would be:
- On a system connected to the internet, copy the container images to a local directory.
- Perform all tests and scans on the local directory, but from a system that is disconnected from the internet. That means the local directory is either removable media or a network file share.
- Once the container images are vetted for internal usage, copy them to an internal container registry that is accessed by internal developers and potentially also internal production systems.
You can perform steps 1 and 3 above with the skopeo copy
command using either of the transports named dir
or oci
. The former is intended to store a single container image in a directory. The latter is intended to store multiple container images in the same directory and includes a JSON index of all images available in the directory.
So, on the internet-connected system:
mkdir /var/images/httpd-ubi:v1.0
skopeo copy docker://quay.io/flozanorht/httpd-ubi:v1.0 dir:/var/images/httpd-ubi:v1.0
And on the disconnected system:
skopeo copy dir:/var/images/httpd-ubi:v1.0 docker://quay.example.com/flozanorht/httpd-ubi:v1.0
If you need to copy several container images at once, the oci
transport can simplify things:
mkdir /var/images/webapp
skopeo copy docker://quay.io/flozanorht/httpd-ubi:v1.0 oci:/var/images/webapp:httpd-ubi:v1.0
skopeo copy docker://quay.io/flozanorht/php-ubi:latest oci:/var/images/webapp:php-ubi:latest
And to publish to a container registry:
skopeo copy oci:/var/images/webapp:httpd-ubi:v1.0 docker://quay.example.com/flozanorht/httpd-ubi:v1.0
skopeo copy oci:/var/images/webapp:php-ubi:latest docker://quay.example.com/flozanorht/php-ubi:latest
Notice that you must use colons (:
) between the name of the directory and the name of the image, and another colon for the image tag.
Warning
Important: If you use the oci
transport with images stored using older image formats, their manifests might be converted to the newer image formats, which will change their digests. Using the dir
transport avoids this risk.
Sync multiple remote images
The skopeo sync
command can be used in many scenarios, such as:
- To copy all tags from a remote container image, which is a great way of keeping a mirror up-to-date, as only new tags and layers will be effectively copied.
- To copy a subset of tags from one or more remote container images.
In the former case, you specify the name of your source and destination registries, for example:
skopeo sync docker://registry.redhat.io/ubi10/ubi docker://quay.example.com/ubi10/ubi
In the latter case, you must provide a YAML file that describes which images to sync. For example:
registry.redhat.io:
images:
ubi10/ubi:
- latest
- "10.0"
- 10.0-1745487123
- 10.0-1747220028
There are many more variations of the syntax for specifying images, and you can check Skopeo's manual pages with the man skopeo sync
command. For example, you can specify image digests or ranges of tags (to specify a range of semantic versions).
More importantly, notice that the YAML syntax allows you to specify different source registries and different image repositories from each registry.
No matter how large or complex your YAML file is, you can mirror all images with a single command:
skopeo sync --src yaml --dest docker sync.yaml quay.example.com
Notice that you specify the transport type for source and destination using their own command-line options instead of including it as a component of the source or destination. This enables the skopeo sync
command to accept the yaml
transport, which other commands do not recognize.
The skopeo sync
command has some limitations compared to the skopeo copy
command. For example, not all transports can be used for the destination. In the disconnected mirror scenario, I find that I can use skopeo sync
to create a directory of offline container images. However, I cannot use skopeo sync
to copy the source directory to my disconnected container registry. My solution in this situation is to use a script similar to the script used for the skopeo copy
of multiple images.
Skopeo on Non-Linux systems
My only "complaint" about Skopeo is that it didn't receive the same cross-platform love from the community and its corporate sponsor as Podman did.
It is arguably easier to port Skopeo to macOS and Windows than it is to port Podman, because outside of Linux systems, Podman also needs to maintain a Linux VM. On the other hand, there's no point porting the remaining set of the container tools unless Podman is working fine and with all its features, which it already is, by the way.
The Skopeo releases GitHub page provides no binaries, but they are included with most Linux distributions. macOS users can use brew to install readily available binaries that are equivalent to the Linux binaries.
Windows users can get native Windows binaries from the winskopeo repository on GitHub. All of the Skopeo capabilities are included with the exception of some more advanced features, such as handling container image signatures.
An alternative for Windows users is to run the original Skopeo for Linux using Windows Subsystem for Linux (WSL). This is particularly useful for developers already working from WSL, but is less than ideal for developers using native Windows tools.
There is much more to Skopeo
This article only scratches the surface of what Skopeo can do. For example, the tool can also:
- Convert from older image manifests to current OCI standards, so you can keep your image digests unchanged as you mirror images.
- Convert older Docker archives (from the
docker save
command) to OCI standards, or just copy them to a container registry, without having to first load them into a local container engine. - Handle two kinds of container image signatures: the older GPG standard from early releases of Podman and the newer standard based on Sigstore.
- Handle other kinds of artifacts that you can nowadays store in container registries, such as Helm charts and AI models.
Not to mention that Skopeo can handle private registries that require authentication. It can also manage TLS certificates, including disabling TLS validation if desired (but this is not recommended).
In fact, Skopeo is commonly used in continuous integration (CI) systems, be it based on Kubernetes-native Tekton pipelines (using the Skopeo container image and its associated Tekton task) or traditional pipelines (based on Jenkins, using the Skopeo binary installed in a Jenkins node).
If you are not using Skopeo today, you are missing many opportunities to improve your container image workflows. There's more to containers than just container engines and Kubernetes, and Skopeo is a tool that should be part of your cloud-native toolbelt.
Acknowledgements
Thanks a lot to Andrew Block, Dan Walsh, Dave Darrah, and Miloslav Trmač for reviewing a draft of this article.