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:
$ 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:dir:An image unpacked into a local directory.
oci:andoci-archive:An image in an OCI layout directory or
tararchive.
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:
$ {
"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.
$ {
"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)):
$ 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:
$ 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:
$ Or save it as an OCI archive:
$ 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:
$ 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:
$ 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:
$ 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:
$ This deletes the manifest, so the tag stops resolving. The registry only reclaims the layers when it runs garbage collection.