Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français
Lire en français

Skopeo: inspect, copy and migrate container images without a daemon

16 min read
Improve this page

Logo Skopeo

Skopeo lets you inspect, copy and delete container images directly on registries, without downloading them or starting a daemon. In 10 minutes, you will know how to transfer an image between Docker Hub and a private registry, build a local mirror, and prepare images for an air-gapped environment. To publish or pull an image with Docker, see Publish and pull an image in a controlled way.

What you will learn

  • Inspect a remote image: read metadata, tags and digest without downloading
  • Copy between registries: transfer images between Docker Hub, Quay.io (in French), Harbor
  • Sync a mirror: replicate several images with a YAML file
  • Handle the air gap: export and import images for disconnected environments

In which context?

As soon as a team manages container images beyond a simple docker pull on a developer workstation, Skopeo becomes indispensable. Here are the concrete situations where it steps in:

  • CI/CD pipeline: copy a freshly built image to a production registry without installing Docker on the runner
  • Corporate mirror: sync a subset of public images to an internal registry (Harbor, Nexus) to control the versions that get deployed
  • Air-gapped environment: export images on a connected workstation, carry them over a physical medium, then import them into an isolated registry
  • Security audit: check the metadata, digests and tags of an image before deploying it, without downloading it
  • Registry migration: move hundreds of images from one Docker Hub to a private registry with a single sync command
  • Multi-architecture: copy multi-arch images (amd64 + arm64) between registries while preserving the manifests

What Skopeo can do

Skopeo covers four main use cases for managing container images:

FeatureCommandUse case
Inspectionskopeo inspectCheck the metadata, architecture and layers before downloading
Copyskopeo copyTransfer an image between registries or to a local file
Synchronizationskopeo syncReplicate several images/tags to a private mirror
Deletionskopeo deleteDelete an image from a remote registry

Supported transports

Skopeo supports several source/destination formats (called "transports"):

TransportFormatDescription
docker://docker://registry/image:tagDocker registry (Docker Hub, Quay.io, Harbor, etc.)
dir:dir:/local/pathLocal folder with the layers and the manifest
docker-archive:docker-archive:/path/image.tarArchive of a single image in the docker-save(1) format, decompressed layers
oci:oci:/path:tagFolder in the OCI format
oci-archive:oci-archive:/path/image.tarArchive in the OCI format, compressed layers kept
containers-storage:containers-storage:image:tagLocal Podman/Buildah storage

Installing Skopeo

Terminal window
sudo apt update && sudo apt install -y skopeo

Check the installation:

Terminal window
skopeo --version

Expected result (the version may vary):

skopeo version 1.22.2
commit: 267465e170820673de25149378284fb352daa65e

Inspect a remote image

The skopeo inspect command lets you read the metadata of an image without downloading it. Handy for checking the architecture, the creation date or the environment variables before deploying.

Display the full metadata

Terminal window
skopeo inspect docker://docker.io/library/nginx:1.27.3

Excerpt of the result:

{
"Name": "docker.io/library/nginx",
"Digest": "sha256:bc2f6a7c8ddbccf55bdb19659ce3b0a92ca6559e86d42677a5a02ef6bda2fcef",
"RepoTags": ["1", "1-alpine", "1.27", "1.27.3", "..."],
"Architecture": "amd64",
"Os": "linux",
"Created": "2024-11-26T18:42:08Z",
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.27.3",
"NJS_VERSION=0.8.7"
]
}

The key pieces of information returned:

  • Digest: unique SHA256 fingerprint of the image (guarantees integrity)
  • RepoTags: all the tags available for this image
  • Architecture/Os: target platform (amd64, arm64, linux, windows)
  • Created: build date of the image
  • Env: environment variables defined in the image

Extract a specific piece of information with jq

To display only some fields, combine it with jq:

Terminal window
skopeo inspect docker://docker.io/library/nginx:1.27.3 | jq '{Name, Digest, Architecture, Os, Created}'

Result:

{
"Name": "docker.io/library/nginx",
"Digest": "sha256:bc2f6a7c8ddbccf55bdb19659ce3b0a92ca6559e86d42677a5a02ef6bda2fcef",
"Architecture": "amd64",
"Os": "linux",
"Created": "2024-11-26T18:42:08Z"
}

List the available tags

To know all the published versions of an image:

Terminal window
skopeo list-tags docker://docker.io/library/nginx

Filter the tags with jq (here, the 1.27.x versions):

Terminal window
skopeo list-tags docker://docker.io/library/nginx | jq '.Tags | map(select(startswith("1.27"))) | .[:10]'

Result:

[
"1.27",
"1.27-alpine",
"1.27-alpine-otel",
"1.27-alpine-perl",
"1.27-alpine-slim",
"1.27-alpine3.19",
"1.27-alpine3.19-otel",
"1.27-alpine3.19-perl",
"1.27-alpine3.19-slim",
"1.27-alpine3.20"
]

Inspection with authentication

If the registry requires authentication:

Terminal window
skopeo inspect --creds user:password docker://registry.example.com/my-image:1.0.0

Or with an authentication file (Docker format):

Terminal window
skopeo inspect --authfile ~/.docker/config.json docker://registry.example.com/my-image:1.0.0

Copy images between registries

The skopeo copy command transfers an image from a source to a destination. The transfer happens directly between the registries, your machine does not store the image.

Basic copy between two registries

Terminal window
skopeo copy docker://docker.io/library/alpine:3.20.6 docker://registry.example.com/alpine:3.20.6

The alpine:3.20.6 image is copied from Docker Hub to your private registry.

Copy with authentication

When the source and/or the destination require credentials:

Terminal window
skopeo copy \
--src-creds source_user:source_password \
--dest-creds dest_user:dest_password \
docker://quay.io/my-org/my-app:2.1.0 \
docker://registry.example.com/my-app:2.1.0

Copy to a local folder

To extract the layers and the manifest into a folder:

Terminal window
skopeo copy docker://docker.io/library/alpine:3.20.6 dir:/tmp/alpine-3.20.6

Content of the created folder:

/tmp/alpine-3.20.6/
├── 0a9a5dfd008f... # Compressed layer
├── ff221270b9fb... # Image configuration
├── manifest.json # Manifest describing the image
└── version # Format version

Copy to a Docker archive

To create a .tar file compatible with docker load, give the archive the name that the image will carry once loaded, after the path of the file:

Terminal window
skopeo copy docker://docker.io/library/alpine:3.20.6 docker-archive:/tmp/alpine-3.20.6.tar:docker.io/library/alpine:3.20.6

Without this destination name, docker load succeeds but only prints Loaded image ID: sha256:…: the image is loaded without a name or tag, and you can only run it by its identifier.

Copy to an OCI archive

OCI format, compatible with podman load and OCI runtimes. The destination name follows the same principle as for docker-archive:: without it, docker load also gives a plain Loaded image ID: sha256:….

Terminal window
skopeo copy docker://docker.io/library/alpine:3.20.6 oci-archive:/tmp/alpine-3.20.6-oci.tar:docker.io/library/alpine:3.20.6

Convert between Docker and OCI formats

From Docker to OCI:

Terminal window
skopeo copy docker://docker.io/library/nginx:1.27.3 oci:nginx-oci:1.27.3

From OCI to a Docker archive:

Terminal window
skopeo copy oci:nginx-oci:1.27.3 docker-archive:nginx.tar:docker.io/library/nginx:1.27.3

Useful options for copying

OptionDescription
--allCopies all the architectures of a multi-arch image
--preserve-digestsKeeps the original digests (useful for signatures)
--retry-times 3Number of attempts on network failure
--retry-delay 5sDelay between attempts (v1.18.0+)
--digestfile /tmp/digest.txtWrites the digest of the copied image to a file

Example with reliability options:

Terminal window
skopeo copy \
--retry-times 3 \
--retry-delay 5s \
--digestfile /tmp/nginx-digest.txt \
docker://docker.io/library/nginx:1.27.3 \
docker://registry.example.com/nginx:1.27.3

Air-gapped (disconnected) environments

In environments cut off from the Internet, Skopeo lets you export then import images without a network connection.

Full air-gap workflow

  1. On a connected machine: export the image to an oci-archive: archive, which keeps the layers compressed. For alpine:3.24.3, it weighs 3.86 MB against 8.7 MB as docker-archive:.

    Terminal window
    skopeo copy docker://docker.io/library/nginx:1.27.3 oci-archive:/media/usb/nginx-1.27.3.tar:docker.io/library/nginx:1.27.3
  2. Transfer the archive to the air-gapped environment (USB stick, disk, etc.)

  3. On the air-gapped machine: import the image into the private registry

    Terminal window
    skopeo copy oci-archive:/media/usb/nginx-1.27.3.tar docker://internal-registry.local/nginx:1.27.3

    This command assumes that internal-registry.local serves trusted HTTPS on port 443: Skopeo verifies the registry's certificate. For a test registry over HTTP, for example on port 5000, give the port and disable TLS verification of the destination:

    Terminal window
    skopeo copy --dest-tls-verify=false oci-archive:/media/usb/nginx-1.27.3.tar docker://internal-registry.local:5000/nginx:1.27.3

If the air-gapped machine loads the archive into a Docker daemon with docker load rather than into a registry, the loaded image carries the digest of the manifest of the copied platform, not that of the index: a FROM pinned on the index digest does not resolve there. The lesson Export an image without a daemon with Skopeo shows it and gives the remedy.

Sync several images

The skopeo sync command replicates several images or tags in a single operation. Ideal for keeping a private mirror up to date.

Syncing a whole repository

Sync all the tags of an image:

Terminal window
skopeo sync --src docker --dest docker docker.io/library/alpine registry.example.com

This command copies all the tags of alpine to your registry.

Selective sync with a YAML file

For fine control, define the images in a YAML file:

sync.yaml
# Source registry: docker.io
docker.io:
images:
# All alpine versions >= 3.19
alpine:
- "3.19"
- "3.20"
- "3.20.6"
# Specific nginx versions
nginx:
- "1.27.3"
- "1.27.3-alpine"
tls-verify: true
# Source registry: quay.io
quay.io:
images:
prometheus/prometheus:
- "v2.54.1"
- "v2.55.0"
tls-verify: true

Run it:

Terminal window
skopeo sync --src yaml --dest docker sync.yaml registry.example.com/mirror/

Useful sync options

OptionDescription
--dry-runSimulates the synchronization without copying
--keep-goingContinues even if an image fails
--scopedPrefixes the images with their source path
--allSyncs all the architectures

Example with a dry run to check before running:

Terminal window
skopeo sync --src yaml --dest docker --dry-run sync.yaml registry.example.com/mirror/

Create a local mirror (folder)

To store the images locally before transferring them:

Terminal window
skopeo sync --src docker --dest dir docker.io/library/alpine /backup/images/

Then push the local mirror to a registry:

Terminal window
skopeo sync --src dir --dest docker /backup/images/ registry.example.com

Delete a remote image

The skopeo delete command deletes an image directly on the registry:

Terminal window
skopeo delete docker://registry.example.com/my-app:1.0.0-obsolete

With authentication:

Terminal window
skopeo delete --creds admin:secret docker://registry.example.com/test-image:dev

Compare two images

To check whether two images are identical (same content), compare their digests:

Terminal window
# Digest of the image on Docker Hub
skopeo inspect docker://docker.io/library/nginx:1.27.3 | jq -r '.Digest'
# Digest of the image on your registry
skopeo inspect docker://registry.example.com/nginx:1.27.3 | jq -r '.Digest'

If both digests are identical, the images are strictly equivalent (same binary content).

Troubleshooting

Error "unauthorized: authentication required"

The registry requires authentication. Solutions:

Terminal window
# Option 1: credentials on the command line
skopeo inspect --creds user:password docker://registry.example.com/image:tag
# Option 2: authentication file
skopeo login registry.example.com
skopeo inspect docker://registry.example.com/image:tag

Error "certificate signed by unknown authority"

The TLS certificate of the registry is not recognized. Solutions:

Terminal window
# Option 1: disable TLS verification (not recommended in production)
skopeo inspect --tls-verify=false docker://registry.example.com/image:tag
# Option 2: specify the certificates folder
skopeo inspect --cert-dir /etc/docker/certs.d/registry.example.com docker://registry.example.com/image:tag

Error "manifest unknown" or "not found"

The image or the tag does not exist. Check:

  1. The exact name of the image and the tag
  2. That you have read permissions
  3. List the available tags: skopeo list-tags docker://registry/image

Timeout on large transfers

For large images, increase the number of attempts:

Terminal window
skopeo copy --retry-times 5 --retry-delay 10s docker://source/image:tag docker://dest/image:tag

Key points

  • Skopeo works without a daemon: unlike Docker, it needs no background service
  • Direct transfer: images go from registry to registry without intermediate storage
  • Pin versions: always use precise tags (e.g. 1.27.3) rather than latest
  • Several formats: Skopeo converts between Docker, OCI, archives and folders
  • Air-gap ready: export/import through archives for disconnected environments

Next steps

More information

FAQ: frequently asked questions

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn