rules_img Overview: Building Container Images Faster with Bazel

Contents

I read Tweag's post, "Announcing rules_img: a faster path to container images in Bazel," and noted its main points. What stood out was that the speed improvement comes less from optimizing the tool and more from a design that changes how data moves. The idea is simple: treat images as metadata first, and move the bytes (layers) only at the end.

Starting with Bazel

Bazel is a build system created by Google. It manages builds, tests, and packaging based on a dependency graph, with strengths in caching and reproducibility. Adding a remote cache or remote build execution (RBE) can reduce build times even in large codebases. rules_img, the subject of this post, is a ruleset for building container images in Bazel.

The Starting Problem: Why Bazel Image Builds Slow Down

Bazel caches build outputs well and provides reproducibility. But adding container images introduces a bottleneck. In the existing rules_oci workflow, the following happens.

In other words, large amounts of data unrelated to the build are moved repeatedly. The slowdown ultimately comes down to the amount of data transferred.

A 2-Minute Overview: OCI Image Components

An image is broadly divided into metadata and layer bytes.

An image therefore consists of the actual layer bytes and JSON pointing to those bytes. rules_img uses this structure directly to optimize performance.

The Core Ideas Behind rules_img

rules_img: 메타데이터는 빌드 그래프로, 레이어 바이트는 끝에서만

rules_img: 메타데이터는 빌드 그래프로, 레이어 바이트는 끝에서만

rules_img: 메타데이터는 빌드 그래프로, 레이어 바이트는 끝에서만pull은 레지스트리에서 manifest와 config JSON만 가져온다. image_layer 액션은 레이어 바이트를 CAS에 두고 메타데이터만 빌드 그래프로 보낸다. image_manifest 조립은 digest와 size만 참조하고, bazel run 시점에 레지스트리에 없는 레이어만 전송한다.메타데이터만메타데이터레이어 바이트메타데이터digest·size만 참조무엇을 푸시할지 명세없는 레이어만 전송레지스트리레이어 블롭은 여기에 둔다pullmanifest·config JSON만image_layer 액션tar와 메타데이터를 함께 만듦CAS레이어 바이트 저장빌드 그래프작은 메타데이터만 흐름image_manifest 조립작은 JSON 작업bazel runpush·load 실행
pull은 manifest와 config만 가져오고, image_layer가 만든 레이어 바이트는 CAS에 두며 빌드 그래프에는 메타데이터만 흐른다. 실제 레이어 전송은 bazel run 때 레지스트리에 없는 것만 일어난다.
Read the diagram as text
  1. pull은 레지스트리에서 manifest와 config JSON만 가져온다. 레이어 블롭은 레지스트리에 둔다.
  2. 가져온 메타데이터가 빌드 그래프로 들어간다.
  3. image_layer 액션이 tar와 메타데이터를 함께 만들고, 레이어 바이트는 CAS에 저장된다.
  4. 같은 액션의 작은 메타데이터만 빌드 그래프로 흐른다.
  5. image_manifest 조립은 레이어의 digest와 size만 참조하는 작은 JSON 작업이다.
  6. 빌드 단계는 무엇을 푸시할지 명세만 만들고, 실제 바이트 이동은 bazel run 시점으로 미룬다.
  7. bazel run은 레지스트리에 없는 레이어만 전송한다.

1) Metadata-Only Pulls

Pulling a base image does not download its layers. Only the manifest and config JSON are fetched, revealing what is present. The layer blobs stay in the registry.

2) Layer Creation in CAS, Metadata in the Graph

A single action creates both the layer tar and its metadata. The layer bytes are stored in CAS, while only compact metadata flows through the build graph.

3) Manifest Assembly Is Just a Small JSON Operation

The image assembly step references only layer digests and sizes. This keeps remote execution inputs small and improves cache efficiency.

4) Defer Push/Load to the run Stage

The build stage only produces a specification of what to push. Actual byte transfer happens at bazel run time, sending only layers missing from the registry. The local machine does not act as an unnecessary intermediary.

Why It Is Faster, in One Sentence

Let only metadata flow through the build graph, and move bytes only at the end.

The post describes the following effects.

Comparing rules_oci and rules_img

Category rules_oci rules_img
Base image pull Download all layers Download only manifest/config
Build inputs Include full layers Metadata-focused
Remote execution Heavy input staging Light input staging
Push method Retransfer all layers Transfer only missing layers
Local load Full reload with docker load Incremental loading based on containerd

Practical Points to Check

A Short Getting-Started Snippet

bazel
# MODULE.bazel
bazel_dep(name = "rules_img", version = "<version>")

pull = use_repo_rule("@rules_img//img:pull.bzl", "pull")

pull(
    name = "ubuntu",
    registry = "index.docker.io",
    repository = "library/ubuntu",
    tag = "24.04",
)
bazel
# BUILD.bazel
load("@rules_img//img:layer.bzl", "image_layer")
load("@rules_img//img:image.bzl", "image_manifest")

image_layer(
    name = "app_layer",
    srcs = {
        "/app/bin/server": "//cmd/server",
    },
)

image_manifest(
    name = "app_image",
    base = "@ubuntu",
    layers = [":app_layer"],
)

Closing Notes

rules_img turns the fact that an image is a combination of metadata and bytes directly into a performance strategy. During the build, it handles only metadata; bytes move only at the end. This simple principle makes a noticeable speed difference in remote execution environments. If image builds are a bottleneck in Bazel-based CI, understanding why rules_img is fast can itself change the direction of your design.

References

Diagram

100%

Drag to pan; pinch or use the wheel to zoom. Keyboard: arrow keys pan, + − zoom, 0 fits, Esc closes. The same content is in “Read the diagram as text” below the figure.