# chunkahAn OCI building tool that takes a flat rootfs and outputs a layered OCI imagewith content-based layers.## Table of Contents- [Motivation](#motivation)- [Highlights](#highlights)- [Installation](#installation)- [Verifying image signatures](#verifying-image-signatures)- [Usage](#usage)- [Splitting an existing image](#splitting-an-existing-image)- [Splitting an image at build time](#splitting-an-image-at-build-time-buildahpodman-only)- [Advanced Usage](#advanced-usage)- [Understanding components](#understanding-components)- [Customizing the layers](#customizing-the-layers)- [Limiting the number of layers](#limiting-the-number-of-layers)- [Output options](#output-options)- [Building from a raw rootfs](#building-from-a-raw-rootfs)- [Customizing the OCI image config and annotations](#customizing-the-oci-image-config-and-annotations)- [Pruning and filtering](#pruning-and-filtering)- [Architecture](#architecture)- [Parallelism](#parallelism)- [Compatibility with bootable (bootc) images](#compatibility-with-bootable-bootc-images)- [Debugging](#debugging)- [Relationship to `zstd:chunked`](#relationship-to-zstdchunked)- [Origins](#origins)## MotivationTraditionally, images built using a `Dockerfile` result in a multi-layered imagewhich model how the `Dockerfile` was written. For example, a separate layeris created for each `RUN` and `COPY` instructions. This can cause poor layercaching on clients pulling these images. A single package change may invalidatea layer much larger than the package itself, requiring re-pulling.When splitting an image into content-based layers, it doesn't matter how thefinal contents of the image were derived. The image is "postprocessed" so thatlayers are created in a way that tries to maximize layer reuse. Commonly, thismeans grouping together related packages. This has benefits at various levels:at the network level (common layers do not need to be re-pulled), at the storagelevel (common layers are stored once), and at the runtime level (e.g. librariesare only mapped once).chunkah allows you to keep building your image as you currently do, and thenperform this content-based layer splitting.## Highlights- **Content agnostic** — Compatible with RPM-based images, but not only. Otherpackage ecosystems can be supported, as well as fully custom content.- **Container-native** — Best used as a container image, either as part of amulti-staged build, or standalone.- **Zero diff** — Apart from modification time, content is never modified.- **Reproducible** — Supports `SOURCE_DATE_EPOCH` for reproducible layers.It is a non-goal to support initial building of the root filesystem itself.Lots of tools for that exist already. It is also currently a non-goal topreprocess the rootfs to remove common sources of non-reproducibility (such as[add-determinism]). This can be done by the image build process itself.## Installationchunkah is primarily intended to be used as a container image:```shellpodman run -ti --rm quay.io/coreos/chunkah --help```However, if you're currently building images using a multi-stage build, itmay be more convenient to `cargo install` the binary into your builder image(whether at runtime or build time if you own the builder image). chunkah is alsopackaged in Fedora, making it easier to do this there.### Verifying image signaturesContainer images are signed using [cosign] keyless signing. You can verifythe signature of an image using cosign v3+:```shellcosign verify \--certificate-oidc-issuer https://token.actions.githubusercontent.com \--certificate-identity-regexp '^https://github\.com/coreos/chunkah/' \quay.io/coreos/chunkah:latest```## UsageThere are two main ways to use chunkah:- splitting an existing image- splitting an image at build time### Splitting an existing image#### Using Podman/BuildahWhen using Podman/Buildah, the most natural way to split an existing image is touse the `Containerfile.splitter`, passing the target image as the `--from`:```shellIMG=quay.io/fedora/fedora-minimal:latestbuildah build --skip-unused-stages=false --from $IMG \--build-arg CHUNKAH_CONFIG_STR="$(podman inspect $IMG)" \https://github.com/coreos/chunkah/releases/download/v0.7.1/Containerfile.splitter```Additional arguments can be passed to chunkah using the CHUNKAH_ARGS buildargument.> [!NOTE]> You must add the `--skip-unused-stages=false` option (see also [this buildah> RFE][buildah-rfe]).>> For Buildah versions before v1.44, this also requires `-v $(pwd):/run/src> --security-opt=label=disable`. Note that this will leave behind an `out/`> directory in the current directory.Another option is using the chunkah image directly and image mounts:```shellIMG=quay.io/fedora/fedora-minimal:latestpodman pull $IMG # image must be available locallyexport CHUNKAH_CONFIG_STR="$(podman inspect $IMG)"podman run --rm --mount=type=image,src=$IMG,dest=/chunkah \-e CHUNKAH_CONFIG_STR quay.io/coreos/chunkah build \-t localhost/fedora-minimal-chunked:latest | podman load```The `-t`/`--tag` option sets the image name in the OCI archive so that `podmanload` automatically tags the loaded image. Without it, the image is loaded as anunnamed image identified only by its digest.For large images, this approach is less efficient than using`Containerfile.splitter`, which avoids the overhead of tarring and untarringthe OCI archive (though the same efficiency can be gained by using[`--output oci:PATH`](#output-options) and importing it using `skopeo copy`.)#### Using Docker/MobyYou can use the chunkah image directly using image mounts (requires v28+):```shellIMG=quay.io/fedora/fedora-minimal:latestdocker pull $IMG # image must be available locallyexport CHUNKAH_CONFIG_STR="$(docker inspect $IMG)"docker run --rm --mount=type=image,src=$IMG,destination=/chunkah \-e CHUNKAH_CONFIG_STR quay.io/coreos/chunkah build \-t localhost/fedora-minimal-chunked:latest | docker load```Note `docker load` support for OCI archives requires the [containerd imagestore] (default on new installations starting from v29+). If using the legacygraph driver, instead of piping directly into `docker load` as above, you canredirect to a file to save the OCI archive, and then use skopeo to convert tothe Docker archive format:```shelldocker run --rm -ti -v $(pwd):/srv:z -w /srv quay.io/skopeo/stable \copy oci-archive:out.ociarchive docker-archive:out.dockerarchive:chunkeddocker load -i out.dockerarchive```### Splitting an image at build time (buildah/podman only)This uses a method called the "`FROM oci:` trick", for lack of a better term.It has the massive advantage of being compatible with a regular `buildah build`flow and also makes it more natural to apply configs to the image, but isspecific to the Podman ecosystem. This *will not* work with Docker.```Dockerfile# Optional; by default base image metadata (like labels) are lost and need to# either be repeated in the final stage or passed in via a build arg like this# one. Use with `--build-arg CHUNKAH_CONFIG_STR=$(podman inspect $IMG)`.ARG CHUNKAH_CONFIG_STRFROM quay.io/fedora/fedora-minimal:latest AS builderRUN dnf install -y git-core && dnf clean allFROM quay.io/coreos/chunkah AS chunkahARG CHUNKAH_CONFIG_STRRUN --mount=type=bind,target=/run/src,rw \--mount=from=builder,target=/chunkah,ro \chunkah build --output oci:/run/src/outFROM oci:outENTRYPOINT ["git"]```> [!NOTE]> When building your image, you must also add the `--skip-unused-stages=false`> option (see also [this buildah RFE][buildah-rfe]), and you cannot use the> `--jobs` option.>> For Buildah versions before v1.44, this also requires `-v $(pwd):/run/src> --security-opt=label=disable`. Note that this will leave behind an `out/`> directory in the current directory.<!-- markdownlint-disable-next-line MD028 -->> [!NOTE]> There is [a known bug][buildah-annotations-bug] in this workflow preventing> informational layer annotations added by chunkah from persisting to the> final image when additional instructions follow the final `FROM`. If you're> interested in that information, you must either use `--config-str` instead> to pass your config, or run chunkah as a separate step as described in the> previous section.## Advanced Usage### Understanding componentsA component is a logical grouping of files that belong together. For example,all files from an RPM belong to the same component. Layers are created based onfound components.A component repo is a source of data from which components can be created. Forexample, the rpmdb is a component repo (one can imagine similar component reposfor other distros). There is also an xattr-based component repo (see the section"Customizing the layers" below). Multiple component repos can be active at once.### Customizing the layersIt is possible to create custom components by setting the `user.component` xattron files/directories. This can be done using `setfattr`, e.g.:```DockerfileRUN setfattr -n user.component -v "custom-apps" /usr/bin/my-app```This is compatible with rpm-ostree's support for [the samefeature](https://coreos.github.io/rpm-ostree/build-chunked-oci/#assigning-files-to-specific-layers).However, unlike rpm-ostree, this does not guarantee unique layers per xattrcomponent.In addition, the `user.update-interval` xattr is also supported. The value isthe average number of days between updates, either as an integer or a namedlabel (`daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `yearly`), e.g.:```DockerfileRUN setfattr -n user.component -v "custom-apps" /usr/bin/my-app && \setfattr -n user.update-interval -v "monthly" /usr/bin/my-app```This only needs to be set on one of the files in the component. If multiplefiles have conflicting values, chunkah reports an error.It is strongly recommended to set this xattr. A rough approximation is fine.This helps the packing algorithm make better decisions about which components togroup together. When missing, defaults to `weekly`.### Limiting the number of layersBy default, the maximum number of layers emitted is 64. This can be increased ordecreased using the `--max-layers` option. If the number of components exceedsthe maximum, chunkah will pack multiple components together. There is thus atradeoff in deciding this. Fewer layers means losing the efficiency gains ofcontent-based layers. Too many layers may mean excessive processing and overheadwhen pushing/pulling the image. Note that containers-storage has a hard limit of500 layers.### Output optionsBy default, chunkah writes an OCI archive to stdout. The `-o`/`--output` flagcontrols where and in what format the image is written. It supports an optionaltransport prefix:- `--output PATH` or `--output oci-archive:PATH` — write an OCI archive to afile instead of stdout.- `--output oci:PATH` — write the OCI directory layout directly to disk. Thisavoids the overhead of tarring and untarring the archive, which isparticularly useful in the buildah `FROM oci:` flow (see [Splitting an imageat build time](#splitting-an-image-at-build-time-buildahpodman-only)).By default, layers are uncompressed (since in the common case the OCIarchive/directory is immediately imported into a container storage backend,which would immediately uncompress it). Use `--compressed` to enable gzipcompression for layers (and the OCI archive itself, if applicable). Thecompression level can be tuned with `--compression-level` (0-9, default 6).### Building from a raw rootfsFor completeness, note it's of course also possible to split any arbitraryrootfs, regardless of where it comes from:```shellpodman run --rm -v /path/to/rootfs:/chunkah:z \-e CHUNKAH_CONFIG_STR="$(cat config.json)" \quay.io/coreos/chunkah build > out.ociarchive```> [!NOTE]> The `:z` option will relabel all files for access by the container, which may> be expensive for a large rootfs. You can use `--security-opt=label=disable` to> avoid this, but it disables SELinux separation with the chunkah container.See [Output options](#output-options) for controlling the output format andcompression.### Customizing the OCI image config and annotationsThe OCI image config can be provided via the `--config` option (as a file) or`--config-str`/`CHUNKAH_CONFIG_STR` (inline). The primary format is the [OCIimage config] spec as JSON:```json{"Entrypoint": ["/bin/bash"],"Cmd": ["-c", "echo hi"],"WorkDir": "/root"}```The output format of `podman inspect` and `docker inspect` are also supported,mostly for convenience when splitting an existing image, though it does alsohave the advantage of capturing annotations. Otherwise, it's also possible toset annotations directly using `--annotation`. Labels can also be added via`--label`.### Pruning and filteringThe `--prune` option excludes paths from the rootfs. It can be specifiedmultiple times. The trailing slash matters:- `--prune /path` excludes the directory and all its descendants entirely.- `--prune /path/` excludes only the contents but keeps the directory itself.By default, chunkah errors when encountering special file types (sockets,FIFOs, block/char devices). Use `--skip-special-files` to silently skip theminstead.### ArchitectureThe `--arch` option overrides the target architecture for the output image. Thisis useful when splitting an image whose architecture differs from the runninghost. If not provided, the architecture from the config is used (if available),or the current system architecture otherwise. Common aliases are supported (e.g.`x86_64` maps to `amd64`, `aarch64` to `arm64`).### ParallelismLayers are written in parallel. The number of threads can be controlled with`-T`/`--threads` (or `CHUNKAH_THREADS`). By default, the number of availableCPUs is used.### Compatibility with bootable (bootc) imageschunkah has no special handling for [bootable container images]. This shouldwork fine for non-OSTree based images (i.e. "plain" images). Packing still needsto be fine-tuned for bootable images (or very large images in general). You willlikely want to increase the default maximum number of layers from 64 (e.g. 128)for better splitting.As mentioned in [thissection](#splitting-an-image-at-build-time-buildahpodman-only), in the buildtime flow, labels from the base image will be lost, including versioninginformation and `containers.bootc=1`, which is required by bootc. So you'll wantto use a `CHUNKAH_CONFIG_STR` build arg or just re-add the label.Using chunkah to rechunk an OSTree-based bootc image is also possible bytransforming it into a plain one by passing `--prune /sysroot/` to strip OSTreedata from the image. If base metadata is persisted (either the existing imageflow, or the build time flow with inspect output passed in as a build arg), youwill need to remove the ostree-related labels using the `--label KEY-` option:```DockerfileARG CHUNKAH_CONFIG_STRFROM quay.io/fedora/fedora-bootc:latest AS builderRUN dnf install -y tmux && dnf clean allRUN bootc container lintFROM quay.io/coreos/chunkah AS chunkahARG CHUNKAH_CONFIG_STRRUN --mount=from=builder,src=/,target=/chunkah,ro \--mount=type=bind,target=/run/src,rw \chunkah build --prune /sysroot/ --max-layers 128 \--label ostree.commit- --label ostree.final-diffid- \--output oci:/run/src/outFROM oci:out```### DebuggingUse `-v` for verbose output or the `RUST_LOG` environment variable forfine-grained control (e.g. `RUST_LOG=chunkah=debug`). Logs are writtento stderr. (There is also `-vv` for trace output mostly meant for chunkahdevelopment. You'll want to redirect stderr to a file!)## Relationship to `zstd:chunked`[zstd:chunked] is a [container-libs] feature that enables partial layer pulls,fetching only changed files and chunks within a layer via HTTP range requests.chunkah and zstd:chunked are complementary:- chunkah operates at build time: it structures the image so that unchangedcontent lives in unchanged layers, maximizing layer-level deduplication. Thisworks with any OCI registry and requires no special client support.- zstd:chunked operates at pull time: within layers, only the files (technicallychunks *within* files) that changed are fetched. This requires client supportand HTTP range requests from the registry, and has higher CPU and memoryoverhead on the client side.Used together, most layers are reused at the registry level (thanks to chunkah),and for the few that *did* change, you can efficiently pull just those (thanksto zstd:chunked), minimizing overhead.## Originschunkah is a generalized successor to rpm-ostree's [build-chunked-oci] commandwhich does content-based layer splitting on RPM-based [bootable containerimages]. Unlike rpm-ostree, chunkah is not tied to bootable containers nor RPMs.The name is a nod to this ancestry and to buildah, with which it integrateswell.[add-determinism]: https://github.com/keszybz/add-determinism[bootable container images]: https://containers.github.io/bootable/[buildah-annotations-bug]: https://github.com/containers/buildah/issues/6652[buildah-rfe]: https://github.com/containers/buildah/issues/6621[build-chunked-oci]: https://coreos.github.io/rpm-ostree/build-chunked-oci/[containerd image store]: https://docs.docker.com/engine/storage/containerd/[container-libs]: https://github.com/containers/container-libs[cosign]: https://github.com/sigstore/cosign[OCI image config]: https://github.com/opencontainers/image-spec/blob/26647a49f642c7d22a1cd3aa0a48e4650a542269/specs-go/v1/config.go#L24[zstd:chunked]: https://github.com/containers/container-libs/blob/main/storage/docs/containers-storage-zstd-chunked.md