SOCI (Seekable OCI) lets a
container runtime lazily pull image layers — fetching only the files a container
actually reads — using a ztoc (a table of contents plus seekable-gzip
checkpoints) per layer, indexed by a SOCI index manifest. rules_img can
produce a SOCI Index Manifest v2 for an image at build time using a pure-Go
ztoc generator (no cgo, no soci binary required).
SOCI v2 (unlike v1) does not use the OCI referrers API. It links a SOCI index to its image with annotations:
- the image manifest gains a
com.amazon.soci.index-digestannotation naming its SOCI index (this changes the image manifest's digest); - the SOCI index is itself an OCI image manifest whose
layersare the ztoc blobs (application/octet-stream) and whose config is the 2-byte{}blob with media typeapplication/vnd.amazon.soci.index.v2+json; - when the image is published as an OCI image index, the index gains an extra
entry for the SOCI index (
artifactType: application/vnd.amazon.soci.index.v2+json, the target platform, and acom.amazon.soci.image-manifest-digestannotation pointing back at the image manifest).
SOCI is opt-in and off by default. Turn it on globally in your .bazelrc (or on
the command line):
common --@rules_img//img/settings:soci=enabled
Two tuning settings match soci-snapshotter's defaults:
# Uncompressed bytes between ztoc checkpoints (default 4 MiB).
common --@rules_img//img/settings:soci_span_size=4194304
# Layers smaller than this get no ztoc and are omitted from the index (default 10 MiB).
common --@rules_img//img/settings:soci_min_layer_size=10485760
The global soci flag is the default for per-layer and per-manifest soci
attributes (all default to auto), mirroring how estargz works:
image_layer(and other layer rules) take asociattribute (auto/enabled/disabled). When effectively enabled and the layer is gzip-compressed, the layer action emits a ztoc as an extra output (theztocoutput group) and records it onSingleLayerInfo.ztoc.image_manifesttakessoci,soci_span_size, andsoci_min_layer_sizeattributes. When effectively enabled it assembles the SOCI index for the image: it reuses each layer's ztoc when present, generates one on the fly for any materialized gzip layer that lacks one, and stamps thecom.amazon.soci.index-digestannotation onto the image manifest.
image_manifest(
name = "app",
base = "@distroless_cc",
layers = [":app_layer"],
soci = "enabled", # or rely on the global flag
)For soci-snapshotter to discover the SOCI index at pull time, the SOCI index
must be cross-referenced from an OCI image index. So wrap the image in an
image_index (this is also how soci convert bundles single-platform images):
image_manifest(name = "app", layers = [":app_layer"], soci = "enabled")
image_index(
name = "app_index",
manifests = [":app"],
)
# app_index's index.json lists the image manifest AND its SOCI index entry
# (com.amazon.soci.image-manifest-digest), the fully discoverable v2 layout.Then, on the node, run soci-snapshotter and pull the image reference published
from app_index; the snapshotter reads the SOCI index entry from the OCI index,
fetches the SOCI index, and lazily mounts the layers. When you push an
image_index, the SOCI index manifest, its {} config, and the ztoc blobs are
uploaded alongside the image automatically.
Bare
image_manifest(noimage_index) is annotation-only. If you publish a singleimage_manifestdirectly, the image manifest still gets thecom.amazon.soci.index-digestannotation and the SOCI index is built, but no OCI index / cross-reference entry is emitted and the SOCI index blobs are not pushed. That artifact is not discoverable by soci-snapshotter on its own. Wrap the image in animage_indexfor the discoverable, pushed layout.
- Enabling SOCI changes the image manifest digest, because the
com.amazon.soci.index-digestannotation is baked into the manifest. This is expected and matchessoci convert. - Only gzip layers get a ztoc. zstd and uncompressed layers, empty layers, and non-tar artifact layers are silently omitted from the SOCI index.
- Layers below
soci_min_layer_sizeare omitted (they don't benefit from lazy pulling). This filter applies to the standaloneimg soci-indexCLI. When building through the Bazel rules, every gzip layer is currently indexed (regardless of size), because a layer's size is only known after its action runs and the ztoc blobs are shipped by position — thesoci_min_layer_sizesetting is accepted but not yet applied on the Bazel path. - Shallow base-image layers (pulled lazily, with no local blob) are skipped on a best-effort basis, since a ztoc cannot be generated without the layer bytes.
- The generated ztoc bytes are byte-for-byte compatible with soci-snapshotter's own ztoc format (v0.9 / zinfo v2).
$ bazel build //path/to:app_index --@rules_img//img/settings:soci=enabled
# The image manifest carries the SOCI index annotation:
$ jq '.annotations' bazel-bin/path/to/app_manifest.json
{ "com.amazon.soci.index-digest": "sha256:…" }
# The SOCI index manifest lists the ztocs:
$ jq . bazel-bin/path/to/app_soci_index.json
# The OCI index cross-references the SOCI index:
$ jq '.manifests[] | select(.artifactType)' bazel-bin/path/to/app_index_index.jsonYou can also produce a ztoc or a SOCI index directly with the img tool:
$ img ztoc --blob layer.tgz --output layer.ztoc
$ img soci-index --layer layer-metadata.json=layer.ztoc \
--os linux --architecture amd64 \
--manifest soci.json --config soci-config.json --descriptor soci-descriptor.json