- ð High Performance - Minimizes data transfer and embraces Build without the Bytes from source code to container runtime
- ðĶ OCI Compliant - Builds standard OCI images compatible with any container runtime
- ð§ Bazel Native - No Docker daemon required, fully hermetic builds
- ð Multi-Platform - Native cross-platform support through Bazel transitions
- ⥠eStargz Support - Lazy pulling optimization for faster container starts
- ðŠķ Smaller layers - Deduplicates files using hardlinks
- ðŊ Shallow Base Images - Avoid downloading layers from huge base images like CUDA
- ðĒ Enterprise Ready - Remote Build Execution and Content Addressable Storage integration
Add to your MODULE.bazel
:
bazel_dep(name = "rules_img", version = "0.1.5")
Configure default settings (optional) in .bazelrc
# The compression algorithm to use ("gzip" or "zstd")
common --@rules_img//img/settings:compress=zstd
# Support for seekable eStargz layers
# with the containerd stargz-snapshotter
common --@rules_img//img/settings:estargz=enabled
# Opt-in to stamping of image_push rules
common --@rules_img//img/settings:stamp=disabled
# The push strategy to use (see below for more info).
# "eager", "lazy", "cas_registry", or "bes"
common --@rules_img//img/settings:push_strategy=eager
# The load strategy to use.
# "eager" or "lazy"
common --@rules_img//img/settings:load_strategy=eager
# The daemon to target with image_load
# "docker" or "containerd"
common --@rules_img//img/settings:load_daemon=docker
# Bazel remote cache to use for lazy pushing of container images.
# Uses the same format as Bazel's --remote_cache flag.
# Falls back to $IMG_REAPI_ENDPOINT env var.
common --@rules_img//img/settings:remote_cache=grpcs://remote.buildbuddy.io
# Credential helper to use for authenticating gRPC connections during push operations
# in some push strategies.
# This can be the same as Bazel's credential helper.
# Falls back to $IMG_CREDENTIAL_HELPER env var.
common --@rules_img//img/settings:credential_helper=grpcs://remote.buildbuddy.io
You also need a credential helper to pull base images from container registries. We recommend tweag-credential-helper. Refer to the container registry setup for more information.
Add base image to MODULE.bazel
:
pull = use_repo_rule("@rules_img//img:pull.bzl", "pull")
pull(
name = "ubuntu",
digest = "sha256:1e622c5f073b4f6bfad6632f2616c7f59ef256e96fe78bf6a595d1dc4376ac02",
registry = "index.docker.io",
repository = "library/ubuntu",
tag = "24.04",
)
pull(
name = "cuda",
digest = "sha256:f353ffca86e0cd93ab2470fe274ecf766519c24c37ed58cc2f91d915f7ebe53c",
registry = "index.docker.io",
repository = "nvidia/cuda",
tag = "12.8.1-cudnn-devel-ubuntu20.04",
)
Compose images in BUILD.bazel
:
load("@rules_img//img:layer.bzl", "image_layer")
load("@rules_img//img:image.bzl", "image_manifest")
# Create a layer from files
image_layer(
name = "app_layer",
srcs = {
"/app/bin/server": "//cmd/server",
"/app/config": "//configs:prod",
},
compress = "zstd", # Use zstd compression (optional, uses global default otherwise)
)
# Build a container image
image_manifest(
name = "app_image",
base = "@ubuntu", # Optional: build "from scratch" without base.
layers = [
":app_layer",
],
config_fragment = "config.json", # Optional image configuration, uses sane defaults.
)
In most cases, you can just use the builtin transitions feature:
load("@rules_img//img:image.bzl", "image_manifest", "image_index")
# Create platform-specific images
image_manifest(
name = "app",
layers = [":app_layer"],
)
# Combine into multi-platform index
image_index(
name = "app",
manifests = [":app_amd"],
platforms = [
"//:linux_amd64",
"//:linux_arm64",
],
)
You are in full control over the images you put into an image index
load("@rules_img//img:image.bzl", "image_manifest", "image_index")
# Create platform-specific images
image_manifest(
name = "app_amd64",
layers = [":app_layer"],
architecture = "amd64",
os = "linux",
)
image_manifest(
name = "app_arm64",
layers = [":app_layer"],
architecture = "arm64",
os = "linux",
)
# Combine into multi-platform index
image_index(
name = "app",
manifests = [
":app_amd64",
":app_arm64",
],
)
load("@rules_img//img:push.bzl", "image_push")
image_push(
name = "push",
image = ":app",
registry = "ghcr.io",
repository = "my-project/app",
tag = "latest",
)
Run with:
bazel run //:push
Both rules_img
and rules_oci
are modern Bazel rulesets for building OCI container images. While they share the goal of hermetic, reproducible container builds, they take fundamentally different architectural approaches.
rules_oci
uses the oci image layout as an on-disk representation of container images at every step (base image pull, oci_image
rule, oci_image_index
rule).
Additionally, rules_oci
chooses to use only off-the-shelf, pre-built tools for assembling images.
rules_img
chooses to use providers that contain just enough information as needed for subsequent steps. We also use customized tools, instead of prebuilt ones.
This results in a more complex implementation, but also allows for interesting optimizations.
- â Shallow base image pulling
- â Layers are produced in a single action
- â Deduplication of layer contents
- â Advanced push strategies
- â eStargz support for lazy pulling
- â Incremental loading into daemons
- API Reference
- Layer Rules
image_layer
- Create layers from fileslayer_from_tar
- Create layers from tar archivesfile_metadata
- Helper for specifying file attributes ofimage_layer
rule.
- Image Rules
image_manifest
- Build single-platform imagesimage_index
- Build multi-platform image indexes
- Push, Pull and Load Rules
image_push
- Push images to registriesimage_load
- Load images into container daemonspull
- Pull base images
- Layer Rules
Unlike rules_oci which downloads all layers of a base image, rules_img uses a "shallow pull" approach. When you reference a base image like CUDA (which can be 10+ GB), rules_img only downloads the manifest and config - not the actual layer blobs. The layers are only downloaded when and if they're needed during push operations.
This results in:
- Faster builds - No waiting for large base image downloads
- Reduced bandwidth - Only download what you actually use
- True Build-without-the-bytes - Other rulesets download base layers to your local machine in a repository rule. This step cannot be remotely executed and is repeated on every machine running Bazel.
Example with a large CUDA base image:
# This won't download the 10GB of CUDA layers!
pull(
name = "cuda",
digest = "sha256:...",
registry = "index.docker.io",
repository = "nvidia/cuda",
)
rules_img produces both the layer blob and its metadata in a single Bazel action. This design has several advantages:
- Remote execution friendly - Single action works better with RBE
- Image Manifest only depends on metadata - In rules_oci, image actions depend on the actual blobs of their base image and layers, which must be available during the manifest writing action.
The metadata includes the layer's digest, size, and diff ID, all computed during layer creation.
When writing a tar layer, rules_img uses hardlinks to deduplicate identical files. This allows for smaller container images.
rules_img offers four sophisticated push strategies compared to rules_oci's traditional approach. These strategies enable:
- Faster CI/CD - Avoid unnecesary file transfer
- Build without the bytes - Never materialize container layers on your local machine
- Scalability - Designed for organizations with thousands of builds per day
Strategy | Description | Use Case | Requirements |
---|---|---|---|
eager |
Traditional push, download all blobs to the machine running Bazel, then uploads all blobs. | Simple deployments | Normal container registry |
lazy |
Checks registry first, skips existing blobs and streams missing blobs from Bazel's remote cache | Faster CI/CD and Build without the Bytes | Bazel remote cache |
cas_registry |
Uses special container registry that is directly connected to Bazel's remote cache | Fast development cycles. | Special container registry (cmd/registry ), Bazel remote cache |
bes |
Image push happens as side-effect of BES upload. Requires self-hosted BES server. | Extremely fast and efficient for large organizations. | Special BES backend (cmd/bes ), Bazel remote cache |
See the Push Strategies Guide for detailed information about each strategy.
rules_img has first-class support for eStargz (enhanced stargz), enabling "lazy pulling" at container runtime. This means:
- Instant container starts - Containers can start before all layers download
- Bandwidth savings - Only accessed files are downloaded
- Seekable layers - Random access to files within compressed layers
Combined with containerd's stargz-snapshotter, this can reduce container startup time from minutes to seconds for large images.
image_layer(
name = "optimized_layer",
srcs = {...},
estargz = "enabled", # Enable seekable compression
)
The same setting can be globally enabled using --@rules_img//img/settings:estargz=enabled
.
Read the stargz-snapshotter documentation for more information.
rules_img loads images incrementally and efficiently by directly interfacing with the containerd API. This provides significant performance advantages over traditional approaches:
- Direct containerd integration - When Docker is configured with containerd storage, rules_img bypasses
docker load
entirely - Incremental blob loading - Only new or changed layers are loaded, existing blobs are skipped
- Streaming architecture - No temporary tar files or buffering entire images in memory
- Platform selection - Load only the platforms you need from multi-platform images
The performance difference is dramatic, especially for large images:
# Load only the platform you need
bazel run //my:image_load -- --platform linux/amd64
# Incremental loading: only new layers are transferred
# Second load of a slightly modified image is near-instant
bazel run //my:image_load # Only changed layers loaded!
When Docker doesn't support containerd storage, rules_img automatically falls back to docker load
with a clear warning about the performance impact.
This is particularly powerful in development workflows where you're iterating on application layers while keeping large base images (like CUDA) unchanged - subsequent loads only transfer your small application layers.
Future Docker Support: Docker is planning to expose its contentstore API in version 29.0.0, which will enable native incremental loading (moby/moby#44369). Once this ships, rules_img will adopt it to provide incremental loading performance even when the containerd socket isn't directly accesible by users. This will bring the same efficiency benefits to all Docker users, regardless of their platform or configuration.
We invite external contributions and are eager to work together with the build systems community. Please refer to the CONTRIBUTING guide to learn more. If you want to check out the code and run a development version, follow the HACKING guide to get started.
Special thanks to Sushain Cherivirala from Stripe for the inspiring BazelCon talk "Building 1300 Container Images in 4 Minutes". This talk introduced the groundbreaking idea of using the Build Event Service (BES) to sync container images between the remote cache and registry as a side effect. While their implementation was based on the now-archived rules_docker and was never published, it laid the conceptual foundation for our BES push strategy. Their work demonstrated how to achieve dramatic performance improvements in container image builds at scale, inspiring many of the optimizations in rules_img.