SKOPEO-COPY(1)

πŸ’– skopeo-copy(1) β€” the copy glow-up πŸ’–

okay bestie, welcome to skopeo-copy(1), aka the one command that takes an image from here and puts it over there and doesn't lose a single crumb on the way. it's giving delivery service but for manifests πŸ’…


🌸 NAME 🌸

skopeo-copy β€” copy an image (manifest, filesystem layers, signatures) from one location to another. that's it, that's the whole job, and it does it flawlessly πŸŽ€


✨ SYNOPSIS ✨

skopeo copy [options] source-image destination-image

like. two names. source and destination. nothing else needed. iconic.


πŸ¦‹ DESCRIPTION πŸ¦‹

Copy an image (manifest, filesystem layers, signatures) from one location to another. everything travels together β€” the manifest, the layers, the signatures β€” like a friend group that refuses to split up 🫢

It uses the system's trust policy to validate images, and rejects images not trusted by the policy. no cap, if the policy says no, it's a no. don't argue with the policy.

  • source-image β€” use the "image name" format described above ✨
  • destination-image β€” use the "image name" format described above ✨

and here's the part people get wrong like every single time, so listen up:

source-image and destination-image are interpreted completely independently β€” e.g. the destination name does not automatically inherit any parts of the source name.

fr fr. they're two separate names, two separate vibes, no inheritance, no hand-me-downs. you spell out the destination yourself or you get surprises. πŸ’€


πŸŽ€ OPTIONS πŸŽ€

also peep skopeo(1) for options placed before the subcommand name β€” those are the global ones, and they live upstream of this whole situation.

  • --additional-tag=strings β€” additional tags. (supports docker-archive.) 🏷️
  • --all, -a β€” if source-image refers to a list of images, instead of copying just the image which matches the current OS and architecture (subject to the use of the global --override-os, --override-arch and --override-variant options), attempt to copy all of the images in the list, and the list itself. go big or go home, bestie πŸ”₯
  • --authfile path β€” path of the primary registry credentials file. On Linux, the default is ${XDG_RUNTIME_DIR}/containers/auth.json. see containers-auth.json(5) for more details about the credential search mechanism and defaults on other platforms. Use skopeo login to manage the credentials. πŸ” The default value of this option is read from the REGISTRY_AUTH_FILE environment variable. (yes, env var, keep it in mind.)
  • --src-authfile path β€” path of the primary registry credentials file for the source registry. uses path given by --authfile, if not provided. 🫢
  • --dest-authfile path β€” path of the primary registry credentials file for the destination registry. uses path given by --authfile, if not provided. 🫢
  • --dest-shared-blob-dir directory β€” directory to use to share blobs across OCI repositories. sharing is caring πŸ’–
  • --digestfile path β€” after copying the image, write the digest of the resulting image to the file. lil receipt of what you just did 🧾
  • --preserve-digests β€” preserve the digests during copying. fail if the digest cannot be preserved. no silent swaps, she said what she said. This option does not change what will be copied; consider using --all at the same time. πŸ‘€
  • --encrypt-layer ints β€” experimental: the 0-indexed layer indices, with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). counting backwards is highkey a superpower 🧚
  • --format, -f manifest-type β€” MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifest type of source, with fallbacks). pick your fighter ✨
  • --help, -h β€” print usage statement. the classic. πŸ™ˆ
  • --multi-arch option β€” control what is copied if source-image refers to a multi-architecture image. default is system. Options:
    • system: copy only the image that matches the system architecture
    • all: copy the full multi-architecture image
    • index-only: copy only the index
    • platform-list: copy only specific platforms (comma-separated list of OS/Architecture pairs, e.g., linux/amd64,linux/arm64)
⚠️ the index-only option and platform-list both create sparse manifest lists, which usually fail unless the referenced per-architecture images are already present in the destination, or the target registry supports sparse indexes. like, usually fail. we warned you πŸ’€

When specifying a platform list, all compression variants and other variations for each platform are copied. no picking and choosing inside a platform, bestie 🌈

  • --quiet, -q β€” suppress output information when copying images. stealth mode 🀫
  • --remove-signatures β€” do not copy signatures, if any, from source-image. Necessary when copying a signed image to a destination which does not support signatures. it's not optional-optional, it's required-optional 🫠
  • --remove-list-signatures β€” do not copy the manifest list signature while preserving per-instance signatures. this provides more granular control than --remove-signatures, which removes all signatures. surgical, not scorched earth πŸ”ͺ✨
  • --strip-removed-platforms β€” when copying only a subset of platforms from a multi-architecture image (using --multi-arch with a platform list), write a manifest list containing only the copied platforms, instead of leaving entries that refer to per-platform images not present in the destination. this is useful for registries that reject manifest lists containing references to missing images. no ghosts in the list πŸ‘»πŸš«
Note: this option requires signatures to be removed using either --remove-signatures or --remove-list-signatures, as stripping instances invalidates the manifest list signature and changes the manifest list digest. The operation will fail if signatures are present and not explicitly removed. read it twice, it's a whole thing 😭
  • --sign-by key-id β€” add a β€œsimple signing” signature using that key ID for an image name corresponding to destination-image. sign it, seal it, deliver it ✍️
  • --sign-by-sigstore param-file β€” add a sigstore signature based on the options in the specified containers sigstore signing parameter file, param-file. see containers-sigstore-signing-params.yaml(5) for details about the file format. 🧾
  • --sign-by-sigstore-private-key path β€” add a sigstore signature using a private key at path for an image name corresponding to destination-image. πŸ”‘
  • --sign-by-sq-fingerprint fingerprint β€” add a β€œsimple signing” signature using a Sequoia-PGP key with the specified fingerprint. sequoia said slay 🌲
  • --sign-passphrase-file path β€” the passphrase to use when signing with --sign-by, --sign-by-sigstore-private-key or --sign-by-sq-fingerprint. Only the first line will be read. A passphrase stored in a file is of questionable security if other users can read this file. Do not use this option if at all avoidable.
😬 that's not me being dramatic, that's the manpage. "of questionable security." "if at all avoidable." take the hint.
  • --sign-identity reference β€” the identity to use when signing the image. The identity must be a fully specified docker reference. If the identity is not specified, the target docker reference will be used. πŸͺͺ
  • --src-shared-blob-dir directory β€” directory to use to share blobs across OCI repositories. yes, the source version of the sharing thing up there πŸ’–
  • --encryption-key protocol:keyfile β€” specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7 (RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@example.com or pkcs7:/path/to/x509-file. πŸ”βœ¨
  • --decryption-key key[:passphrase] β€” key to be used for decryption of images. Key can point to keys and/or certificates. Decryption will be tried with all keys. If the key is protected by a passphrase, it is required to be passed in the argument and omitted otherwise. πŸ—οΈ
  • --src-creds username[:password] β€” credentials for accessing the source registry. 🫢
  • --dest-compress β€” compress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source). squeeze it πŸ—œοΈ
  • --dest-decompress β€” decompress tarball image layers when saving to directory using the 'dir' transport. (default is same compression type as source). un-squeeze it 🫧
  • --dest-oci-accept-uncompressed-layers β€” allow uncompressed image layers when saving to an OCI image using the 'oci' transport. (default is to compress things that aren't compressed). let the layers be free 😌
  • --dest-creds username[:password] β€” credentials for accessing the destination registry. 🫢
  • --src-cert-dir path β€” use certificates at path (*.crt, *.cert, *.key) to connect to the source registry or daemon. πŸ“œ
  • --src-no-creds β€” access the registry anonymously. incognito mode, no name no face πŸ•ΆοΈ
  • --src-tls-verify=bool β€” require HTTPS and verify certificates when talking to container source registry or daemon. Default to source registry setting. don't turn this off unless you know exactly what you're doing, bestie πŸ™
  • --dest-cert-dir path β€” use certificates at path (*.crt, *.cert, *.key) to connect to the destination registry or daemon. πŸ“œ
  • --dest-no-creds β€” access the registry anonymously. πŸ•ΆοΈ
  • --dest-tls-verify=bool β€” require HTTPS and verify certificates when talking to container destination registry or daemon. Default to destination registry setting. πŸ™
  • --src-daemon-host host β€” copy from docker daemon at host. If host starts with tcp://, HTTPS is enabled by default. To use plain HTTP, use the form http:// (default is unix:///var/run/docker.sock). 🐳
  • --dest-daemon-host host β€” copy to docker daemon at host. If host starts with tcp://, HTTPS is enabled by default. To use plain HTTP, use the form http:// (default is unix:///var/run/docker.sock). 🐳
  • --dest-compress-format format β€” specifies the compression format to use. Supported values are: gzip, zstd and zstd:chunked. zstd:chunked is incompatible with encrypting images, and will be treated as zstd with a warning in that case. read that again if you're encrypting 🫠
  • --dest-compress-level format β€” specifies the compression level to use. The value is specific to the compression algorithm used, e.g. for zstd the accepted values are in the range 1-20 (inclusive), while for gzip it is 1-9 (inclusive). different numbers for different algorithms, don't mix them up πŸ”’
  • --dest-force-compress-format β€” ensures that the compression algorithm set in --dest-compress-format is used exclusively. no backups, no substitutes, only the one you picked πŸ’…
  • --src-registry-token token β€” bearer token for accessing the source registry. 🎟️
  • --dest-registry-token token β€” bearer token for accessing the destination registry. 🎟️
  • --dest-precompute-digests β€” precompute digests to ensure layers are not uploaded that already exist on the destination registry. Layers with initially unknown digests (ex. compressing "on the fly") will be temporarily streamed to disk. doing the math first so you don't upload the same layer twice β€” efficiency icon 🧠✨
  • --retry-times β€” the number of times to retry. By default, no retries are attempted. zero. none. first try is the only try unless you say otherwise 😀
  • --retry-delay β€” fixed delay between retries. If not set (or set to 0s), retry wait time will be exponentially increased based on the number of failed attempts. backoff era πŸ“ˆ
  • --src-username β€” the username to access the source registry. πŸ™‹
  • --src-password β€” the password to access the source registry. 🀐
  • --dest-username β€” the username to access the destination registry. πŸ™‹
  • --dest-password β€” the password to access the destination registry. 🀐
  • --image-parallel-copies n β€” maximum number of image layers to be copied (pulled/pushed) simultaneously. Not setting this field will fall back to containers/image defaults. multitasking, but make it a number πŸƒβ€β™€οΈπŸƒβ€β™€οΈπŸƒβ€β™€οΈ

πŸ’« EXAMPLES πŸ’«

Ok bestie, this is the part where it all clicks. Real commands, real copy-paste energy. 🫢


✨ just... copy a thing ✨

Wanna yeet an image from one registry to another? That's literally it:

$ skopeo copy docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest

No drama, no local daemon, just vibes and bytes moving. πŸ’…


πŸŽ€ copy layers to a local directory πŸŽ€

Say you want the actual layers of the docker.io busybox image just sitting in a dir on your machine. Make the dir first (it's giving responsible):

$ mkdir -p /var/lib/images/busybox
$ skopeo copy docker://busybox:latest dir:/var/lib/images/busybox
$ ls /var/lib/images/busybox/*
  /tmp/busybox/2b8fd9751c4c0f5dd266fcae00707e67a2545ef34f9a29354585f93dac906749.tar
  /tmp/busybox/manifest.json
  /tmp/busybox/8ddc19f16526912237dd8af81971d5e4dd0587907234be2b83e249518d5b673f.tar

So you end up with the .tar layers plus manifest.json chilling together. Iconic. πŸ“¦


πŸ¦‹ the docker-archive moment πŸ¦‹

Wanna build an archive that docker load can eat? Bet:

$ skopeo copy docker://busybox:latest docker-archive:archive-file.tar:busybox:latest
⚠️ heads up though β€” the manpage is very clear on this: using a registry is almost always more efficient than doing the archive thing. So like, it works, but it's not the main character here. 😭

πŸ’– copy AND sign πŸ’–

Feeling cryptography-coded today? Sign it on the way through:

$ skopeo copy --sign-by dev@example.com containers-storage:example/busybox:streaming docker://example/busybox:gold

Signed, sealed, slayed. ✍️


🌈 multi-arch: pick your platforms 🌈

Copy only specific platforms out of a multi-architecture image β€” this creates a sparse manifest list:

$ skopeo copy --multi-arch=linux/amd64,linux/arm64 docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest

And if your registry is being a hater and doesn't support sparse lists? Strip it and drop the list signatures:

$ skopeo copy --multi-arch=linux/amd64,linux/arm64 --strip-removed-platforms --remove-list-signatures docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest

Fixed. Periodt. πŸ”₯


πŸ” encrypt an image πŸ”

First, get your image into oci: land:

$ skopeo copy docker://docker.io/library/nginx:1.17.8 oci:local_nginx:1.17.8

Then generate yourself a key pair, because we're doing security today:

$ openssl genrsa -out private.key 1024
$ openssl rsa -in private.key -pubout > public.key

And now encrypt with the public key:

$ skopeo copy --encryption-key jwe:./public.key oci:local_nginx:1.17.8 oci:try-encrypt:encrypted

Obsessed with how clean that is. 🧚


πŸ™ˆ decrypt an image πŸ™ˆ

Got the private key? Then you get the image back:

$ skopeo copy --decryption-key ./private.key oci:try-encrypt:encrypted oci:try-decrypt:decrypted

πŸ’€ copy it still encrypted πŸ’€

Sometimes you don't even wanna peek inside. Just move it, encrypted as-is:

$ skopeo copy oci:try-encrypt:encrypted oci:try-encrypt-copy:encrypted

Zero decryption, zero curiosity. Respect. 😌


πŸ“ decrypt with multiple keys πŸ“

Some encrypted images need more than one key to unlock. Stack those --decryption-key flags like the absolute legend you are:

$ skopeo copy --decryption-key ./private1.key --decryption-key ./private2.key --decryption-key ./private3.key oci:try-encrypt:encrypted oci:try-decrypt:decrypted

Three keys, one copy. It's giving heist movie. πŸ‘‘


⭐ partial encryption (yes, really) ⭐

Container images can also be partially encrypted β€” you do it by specifying the index of the layer. And this is the important bit, so listen up:

  • Layers are 0-indexed indices
  • Negative indexing is supported too
  • So 0 is the first layer, and -1 is the last layer

Let's say the image docker.io/library/nginx:1.17.8 is made up of 3 layers, and out of those 3 you only want to encrypt the 2nd layer:

$ skopeo copy --encryption-key jwe:./public.key --encrypt-layer 1 oci:local_nginx:1.17.8 oci:try-encrypt:encrypted

See how the 2nd layer is 1? That's the 0-indexing doing its thing. 🧠✨


πŸ”— SEE ALSO πŸ”—

Go read these next, iykyk:


πŸ‘‘ AUTHORS πŸ‘‘

Absolute icons who made this happen, put some respect on their names:

  • Antonio Murdaca runcom@redhat.com ⟨mailto:runcom@redhat.com⟩
  • Miloslav Trmac mitr@redhat.com ⟨mailto:mitr@redhat.com⟩
  • Jhon Honce jhonce@redhat.com ⟨mailto:jhonce@redhat.com⟩

That's the whole page bestie! πŸŽ€ The footer on this one is just skopeo-copy(1) with a big ol' empty () where a version and date would normally go β€” so no version stamp, no date stamp, just the page name doing its thing. Cute, mysterious, leaves you wanting more. πŸ’œπŸ’«

Now go copy something. Slay. πŸ¦‹πŸ”₯

πŸ“¦ where this page comes from