Skip to main content

Container Building (OCI SDK)

RBS includes a built-in OCI SDK for building container images as part of your build process. No Docker daemon or Dockerfile is required — images are assembled programmatically using RBS DSL primitives.

Why RBS for Containers?

FeatureRBS OCIDocker BuildBuildpacks
Daemon RequiredNoYesYes
ReproducibleYes (hermetic)PartialPartial
Layer ControlFullLimitedNone
Build IntegrationNativeSeparate toolSeparate tool
Base Image PullBuilt-inBuilt-inBuilt-in

Quick Start

Using High-Level Rules

The simplest way to build container images:
load("@rbs//java/rules.rbs", "java_binary")
load("@rbs//oci/rules.rbs", "oci_image")

# Build your application
java_binary(
    name = "server",
    srcs = glob(["src/**/*.java"]),
    main = "com.example.Server",
    deps = ["@external://spring_boot_web"],
)

# Package it as a container image
oci_image(
    name = "server_image",
    binary = ":server",
    base = "eclipse-temurin:17-jre",
    tag = "my-app:latest",
    ports = [8080],
    env = {"SPRING_PROFILES_ACTIVE": "production"},
)
# Build the image
rbs build :server_image

# The image tar is saved to .rbs/bin/{platform}/server_image/image.tar
# Load it into Docker:
docker load < .rbs/bin/darwin-arm64/server_image/image.tar

OCI SDK Primitives

For advanced use cases, the OCI SDK provides low-level primitives accessible via ctx.oci within custom rules.

ctx.oci.create_layer()

Creates a single OCI layer from files.
def _my_image_impl(ctx):
    # Create a layer with application files
    app_layer = ctx.oci.create_layer(
        name = "app",
        files = {
            "/app/server.jar": ctx.bin.output + "/server.jar",   # dest: source
            "/app/config.yaml": "config/production.yaml",
        },
        directory = "/app",             # Set working directory
        entrypoint = ["java", "-jar", "/app/server.jar"],
    )
    
    # Create a layer with static assets
    assets_layer = ctx.oci.create_layer(
        name = "assets",
        files = {
            "/app/static/": "frontend/dist/",   # Copy entire directory
        },
    )

Layer Parameters

ParameterTypeDescription
namestringUnique name for the layer.
filesdictMapping of destination_path: source_path.
tarstringPath to an existing tar file to use as the layer.
directorystringSets the working directory (WORKDIR).
entrypointlistSets the container entrypoint.
cmdlistSets the container CMD (default arguments).
envdictEnvironment variables to set.
portslistPorts to expose.
userstringUser/group to run as (e.g., "1000:1000").
labelsdictOCI image labels.

ctx.oci.pull()

Pulls a base image from a registry.
def _my_image_impl(ctx):
    # Pull a base image
    base = ctx.oci.pull(
        image = "ubuntu:22.04",
        platform = "linux/amd64",         # Optional: specify platform
    )
    
    # Pull from a private registry
    private_base = ctx.oci.pull(
        image = "registry.example.com/base:latest",
        credentials = {
            "username": "env:REGISTRY_USER",
            "password": "env:REGISTRY_PASS",
        },
    )

ctx.oci.write_docker_tar()

Assembles layers into a Docker-compatible image tarball.
def _my_image_impl(ctx):
    base = ctx.oci.pull(image = "ubuntu:22.04")
    
    app_layer = ctx.oci.create_layer(
        name = "app",
        files = {"/app/main": ctx.bin.executable},
        entrypoint = ["/app/main"],
    )
    
    # Write the final image
    ctx.oci.write_docker_tar(
        output = ctx.bin.output + "/image.tar",
        base = base,
        layers = [app_layer],
        tag = "my-app:latest",
        config = {
            "Env": ["APP_ENV=production"],
            "ExposedPorts": {"8080/tcp": {}},
            "WorkingDir": "/app",
        },
    )

ctx.oci.image_builder()

A fluent builder API for creating images step by step.
def _my_image_impl(ctx):
    builder = ctx.oci.image_builder(base = "node:20-slim")
    
    # Add layers fluently
    builder.add_layer(
        name = "deps",
        files = {"/app/node_modules/": ctx.bin.run_files + "/node_modules/"},
    )
    
    builder.add_layer(
        name = "app",
        files = {"/app/": ctx.bin.run_files + "/src/"},
        directory = "/app",
        entrypoint = ["node", "index.js"],
    )
    
    builder.set_env({"NODE_ENV": "production"})
    builder.expose_ports([3000])
    builder.set_user("1000:1000")
    
    # Build and write
    builder.write(
        output = ctx.bin.output + "/image.tar",
        tag = "my-node-app:latest",
    )

Complete Examples

Java Spring Boot Container

load("@rbs//java/rules.rbs", "java_binary")
load("@rbs//oci/rules.rbs", "oci_image")

java_binary(
    name = "api_server",
    srcs = glob(["src/**/*.java"]),
    main = "com.example.api.Application",
    deps = [
        "@external://spring_boot_web",
        "@external://spring_boot_actuator",
    ],
    javaopts = ["-Xmx512m"],
)

oci_image(
    name = "api_image",
    binary = ":api_server",
    base = "eclipse-temurin:17-jre-alpine",
    tag = "api-server:latest",
    ports = [8080, 8081],
    env = {
        "SPRING_PROFILES_ACTIVE": "production",
        "JAVA_OPTS": "-Xmx512m -XX:+UseG1GC",
    },
    labels = {
        "org.opencontainers.image.title": "API Server",
        "org.opencontainers.image.version": "1.0.0",
    },
)

Node.js Express Container

load("@rbs//nodejs/rules.rbs", "nodejs_binary")
load("@rbs//oci/rules.rbs", "oci_image")

nodejs_binary(
    name = "web_app",
    srcs = glob(["src/**/*.js"]),
    main = "src/index.js",
    deps = [
        "@external://express_repo",
        "@external://cors_repo",
    ],
)

oci_image(
    name = "web_image",
    binary = ":web_app",
    base = "node:20-slim",
    tag = "web-app:latest",
    ports = [3000],
    env = {"NODE_ENV": "production"},
)

Python Django Container

load("@rbs//python/rules.rbs", "py_binary")
load("@rbs//oci/rules.rbs", "oci_image")

py_binary(
    name = "django_app",
    srcs = glob(["**/*.py"]),
    main = "manage.py",
    deps = [
        "@external://django_repo",
        "@external://gunicorn_repo",
    ],
    args = ["runserver", "0.0.0.0:8000"],
)

oci_image(
    name = "django_image",
    binary = ":django_app",
    base = "python:3.11-slim",
    tag = "django-app:latest",
    ports = [8000],
    env = {
        "DJANGO_SETTINGS_MODULE": "myapp.settings.production",
        "PYTHONDONTWRITEBYTECODE": "1",
    },
)

Multi-Stage Build (Custom Rule)

For maximum control over the image layout:
load("@native//:defs", "native")

def _optimized_image_impl(ctx):
    dirs = ctx.bin.create_dirs()
    
    # Pull base image
    base = ctx.oci.pull(image = ctx.attr.base)
    
    # Layer 1: Runtime dependencies (rarely changes — cached efficiently)
    deps_layer = ctx.oci.create_layer(
        name = "dependencies",
        files = {"/app/deps/": dirs.run_files + "/deps/"},
    )
    
    # Layer 2: Application code (changes frequently)
    app_layer = ctx.oci.create_layer(
        name = "application",
        files = {"/app/": dirs.run_files + "/src/"},
        directory = "/app",
        entrypoint = ctx.attr.entrypoint,
        env = ctx.attr.env,
        ports = ctx.attr.ports,
        user = "1000:1000",
    )
    
    # Write the image
    ctx.oci.write_docker_tar(
        output = dirs.output + "/image.tar",
        base = base,
        layers = [deps_layer, app_layer],
        tag = ctx.attr.tag,
    )

native.define_rule(
    name = "optimized_image",
    kind = "container",
    implementation = _optimized_image_impl,
    attrs = {
        "binary": attr.label(is_dep = True, mandatory = True),
        "base": attr.string(default = "ubuntu:22.04"),
        "tag": attr.string(mandatory = True),
        "entrypoint": attr.string_list(default = []),
        "env": attr.string_dict(default = {}),
        "ports": attr.list(default = []),
    },
)

Image Optimization Tips

Place rarely-changing content (OS packages, runtime dependencies) in early layers and frequently-changing content (application code) in later layers. This maximizes layer cache reuse.
Use slim or distroless base images to reduce image size and attack surface:
  • eclipse-temurin:17-jre-alpine instead of eclipse-temurin:17-jdk
  • node:20-slim instead of node:20
  • python:3.11-slim instead of python:3.11
Build images for multiple platforms by specifying the target platform:
oci_image(
    name = "multi_arch_image",
    binary = ":server",
    base = "ubuntu:22.04",
    platforms = ["linux/amd64", "linux/arm64"],
)
Always run containers as a non-root user:
app_layer = ctx.oci.create_layer(
    name = "app",
    files = {...},
    user = "1000:1000",
)

Output

Built images are saved as Docker-compatible tarballs:
.rbs/bin/{platform}/{package}/{target_name}/
└── image.tar       # Docker-compatible tar file

Loading into Docker

docker load < .rbs/bin/darwin-arm64/server_image/image.tar
docker run -p 8080:8080 my-app:latest

Pushing to a Registry

docker load < .rbs/bin/darwin-arm64/server_image/image.tar
docker tag my-app:latest registry.example.com/my-app:latest
docker push registry.example.com/my-app:latest