Skopeo for system admins

Skopeo works with container images and image registries. It needs no daemon and no root privileges. You can inspect a remote image before you deploy it, copy images between registries or into local storage, mirror whole repositories, and delete images from a registry.

Install Skopeo

skopeo(1) is available in Ubuntu:

$
sudo apt install skopeo

Image names

Many Skopeo commands take an image name prefixed by a transport, which specifies where the image lives:

docker://

An image in a remote registry, for example docker://docker.io/ubuntu:26.04.

docker-daemon:

An image in the local Docker daemon storage.

containers-storage:

An image in the local storage used by Podman and Buildah.

dir:

An image unpacked into a local directory.

oci: and oci-archive:

An image in an OCI layout directory or tar archive.

See containers-transports(5) for the full list and the exact syntax of each transport.

Inspect a remote image

skopeo-inspect(1) downloads only the image metadata, not its layers, so you can check what an image contains before you pull it:

$
skopeo inspect docker://docker.io/ubuntu:26.04
{
    "Name": "docker.io/library/ubuntu",
    "Digest": "sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b",
    "RepoTags": [
        "24.04",
        "25.10",
        "26.04",
        "latest"
    ],
    "Created": "2026-08-17T09:00:47.315779976Z",
    "DockerVersion": "",
    "Labels": {
        "org.opencontainers.image.created": "2026-08-17T09:02:45.677319+00:00",
        "org.opencontainers.image.title": "ubuntu",
        "org.opencontainers.image.version": "26.04"
    },
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:06e9d71331fb2b620a4f6c8064e0f84b284bb69a42c7c57b1c962bd4a4cdee76",
        "sha256:f3db1cd940786339b09d8a60e47c66fea9502d788e6fab5bec91a4a77d4ced1c"
    ],
    "LayersData": [
        {
            "MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "Digest": "sha256:06e9d71331fb2b620a4f6c8064e0f84b284bb69a42c7c57b1c962bd4a4cdee76",
            "Size": 41569203,
            "Annotations": {
                "ci.umo.uncompressed_blob_size": "111523840"
            }
        },
        {
            "MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "Digest": "sha256:f3db1cd940786339b09d8a60e47c66fea9502d788e6fab5bec91a4a77d4ced1c",
            "Size": 393,
            "Annotations": {
                "ci.umo.uncompressed_blob_size": "10240"
            }
        }
    ],
    "Env": [
        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ]
}

You can use skopeo-list-tags(1) to list the available tags in a repository.

$
skopeo list-tags docker://docker.io/ubuntu
{
    "Repository": "docker.io/library/ubuntu",
    "Tags": [
        "22.04",
        "24.04",
        "26.04",
        "latest"
    ]
}

Authenticate to a registry

Public images need no credentials. For a private registry, log in with skopeo-login(1). Skopeo stores the credentials in ${XDG_RUNTIME_DIR}/containers/auth.json (see containers-auth.json(5)):

$
skopeo login registry.example.com
Username: myuser
Password:
Login Succeeded!

Alternatively, pass credentials per command with --creds user:password (for inspect and delete), or --src-creds and --dest-creds (for copy).

Skopeo can also automatically use authentication credentials set by docker login, podman login or buildah login.

Copy images

Between registries

Copying an image into an internal registry with skopeo-copy(1) doesn’t require a local daemon, and the image is never unpacked while copying:

$
skopeo copy \ > docker://docker.io/ubuntu:26.04 \ > docker://registry.example.com/mirror/ubuntu:26.04
Getting image source signatures
Copying blob 06e9d71331fb done
Copying blob f3db1cd94078 done
Copying config af52039db3 done
Writing manifest to image destination

By default, Skopeo only copies the image that matches the architecture of your machine. Add --all to copy every architecture of a multi-architecture image.

Both copy and sync can sign the images they write with --sign-by or --sign-by-sigstore, but the signatures are only checked if you configure a trust policy. Refer to containers-policy.json(5) and containers-registries.d(5) for more details.

To and from local storage

Copy a remote image into the local Docker daemon:

$
skopeo copy docker://docker.io/ubuntu:26.04 docker-daemon:ubuntu:26.04

Or save it as an OCI archive:

$
skopeo copy docker://docker.io/ubuntu:26.04 oci-archive:ubuntu-26.04.tar:ubuntu:26.04

The same command works in reverse, with the archive as the source and a registry as the destination.

Mirror multiple images

copy handles one image at a time, while skopeo-sync(1) copies a set of images in a single run and leaves out anything the destination already has. When using sync, specify the transports with --src and --dest instead of prefixing the image names.

To sync a single image, or a whole repository, into an internal registry:

$
skopeo sync --src docker --dest docker docker.io/ubuntu:26.04 registry.example.com/mirror
INFO[0000] Tag presence check                            imagename="docker.io/ubuntu:26.04" tagged=true
INFO[0000] Copying image ref 1/1                         from="docker://ubuntu:26.04" to="docker://registry.example.com/mirror/ubuntu:26.04"
Getting image source signatures
Copying blob 06e9d71331fb done
Copying blob f3db1cd94078 done
Copying config af52039db3 done
Writing manifest to image destination
INFO[0005] Synced 1 images from 1 sources

A source repository without a tag syncs every tag in it.

You can also sync to a directory. The --scoped option keeps the source registry name in the path so that images from different registries cannot collide:

$
skopeo sync --src docker --dest dir --scoped docker.io/ubuntu:26.04 /media/usb

This writes the image to /media/usb/docker.io/library/ubuntu:26.04. To sync it back into a registry, use --src dir --dest docker.

Syncing from a YAML file

When you run the same mirror repeatedly, list the images in a YAML file and pass --src yaml. You can name tags one by one, or match them with a regular expression:

docker.io:
  images:
    ubuntu:
      - "24.04"
      - "26.04"
  images-by-tag-regex:
    nginx: ^1\.2[0-9]-alpine$

Check what a sync would do before you run it with --dry-run:

$
skopeo sync --src yaml --dest docker --dry-run sync.yaml registry.example.com/mirror
INFO[0000] Processing repo                               registry=docker.io repo=ubuntu
INFO[0000] Processing repo                               registry=docker.io repo=nginx
INFO[0000] Querying registry for image tags              registry=docker.io repo=nginx
INFO[0000] Getting tags                                  image=docker.io/library/nginx
WARN[0001] Running in dry-run mode
INFO[0001] Would have copied image ref 1/2               from="docker://ubuntu:24.04" to="docker://registry.example.com/mirror/ubuntu:24.04"
INFO[0001] Would have copied image ref 2/2               from="docker://ubuntu:26.04" to="docker://registry.example.com/mirror/ubuntu:26.04"
INFO[0001] Would have copied image ref 1/10              from="docker://nginx:1.20-alpine" to="docker://registry.example.com/mirror/nginx:1.20-alpine"
INFO[0001] Would have copied image ref 2/10              from="docker://nginx:1.21-alpine" to="docker://registry.example.com/mirror/nginx:1.21-alpine"
INFO[0001] Would have copied image ref 3/10              from="docker://nginx:1.22-alpine" to="docker://registry.example.com/mirror/nginx:1.22-alpine"
INFO[0001] Would have copied image ref 4/10              from="docker://nginx:1.23-alpine" to="docker://registry.example.com/mirror/nginx:1.23-alpine"
INFO[0001] Would have copied image ref 5/10              from="docker://nginx:1.24-alpine" to="docker://registry.example.com/mirror/nginx:1.24-alpine"
INFO[0001] Would have copied image ref 6/10              from="docker://nginx:1.25-alpine" to="docker://registry.example.com/mirror/nginx:1.25-alpine"
INFO[0001] Would have copied image ref 7/10              from="docker://nginx:1.26-alpine" to="docker://registry.example.com/mirror/nginx:1.26-alpine"
INFO[0001] Would have copied image ref 8/10              from="docker://nginx:1.27-alpine" to="docker://registry.example.com/mirror/nginx:1.27-alpine"
INFO[0001] Would have copied image ref 9/10              from="docker://nginx:1.28-alpine" to="docker://registry.example.com/mirror/nginx:1.28-alpine"
INFO[0001] Would have copied image ref 10/10             from="docker://nginx:1.29-alpine" to="docker://registry.example.com/mirror/nginx:1.29-alpine"
INFO[0001] Would have synced 12 images from 2 sources

Add --keep-going if you want the sync to carry on when one image fails instead of stopping.

Delete an image from a registry

Use skopeo-delete(1) to remove an image:

$
skopeo delete docker://registry.example.com/mirror/ubuntu:26.04

This deletes the manifest, so the tag stops resolving. The registry only reclaims the layers when it runs garbage collection.

Further reading