Bazel oci expert
Skill kinhluan/rules-quarkus-skills/.agent-skills/bazel-oci-expert
π€ Complete AI expert ecosystem for Modern Java, Quarkus & Bazel development βοΈβ‘οΈ Coverage for Vert.x, GraalVM, Maven/Gradle migration, and more π
npx -y skills add kinhluan/rules-quarkus-skills --skill bazel-oci-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Expert knowledge for building OCI container images with Bazel using rules_oci. Use for containerizing Quarkus/Java applications, multi-arch builds, and image optimization.
SKILL.md
12.8 KB, as published. Nobody here has run it
bazel-oci-expert
Keyword: oci | Platforms: gemini,claude,codex
Bazel OCI Container Expert Skill - Building, optimizing, and publishing OCI-compliant container images using rules_oci for Quarkus and Java applications.
Core Mandates
- rules_oci First: Use
rules_oci(notrules_docker) for OCI-compliant image builds.rules_dockeris deprecated. - Hermetic Images: All image layers must be built hermetically within Bazel - no
docker build. - Distroless Base: Prefer distroless or minimal base images (e.g.,
gcr.io/distroless/java21-debian12) for security. - Multi-Arch: Build for both AMD64 and ARM64 using platform transitions.
- Layer Caching: Separate application code from dependencies to maximize layer cache hits.
Setup (Bzlmod)
MODULE.bazel
bazel_dep(name = "rules_oci", version = "2.0.0")
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
# Pull base images
oci.pull(
name = "distroless_java21",
digest = "sha256:abc123...", # Pin by digest, not tag
image = "gcr.io/distroless/java21-debian12",
platforms = [
"linux/amd64",
"linux/arm64",
],
)
oci.pull(
name = "ubi_minimal",
digest = "sha256:def456...",
image = "registry.access.redhat.com/ubi9/ubi-minimal",
platforms = [
"linux/amd64",
"linux/arm64",
],
)
use_repo(oci, "distroless_java21", "ubi_minimal")
WORKSPACE.bazel (Legacy - avoid if using Bzlmod)
load("@rules_oci//oci:pull.bzl", "oci_pull")
oci_pull(
name = "distroless_java21",
digest = "sha256:abc123...",
image = "gcr.io/distroless/java21-debian12",
)
Building Container Images
Basic Java Application Image
# services/user/BUILD.bazel
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball")
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
# Package the JAR into a tar layer
pkg_tar(
name = "app_layer",
srcs = [":user-service_deploy.jar"], # java_binary with deploy_env
package_dir = "/app",
)
# Package dependencies into a separate layer
pkg_tar(
name = "deps_layer",
srcs = [":user-service-deps.jar"],
package_dir = "/app/lib",
)
# Build the OCI image
oci_image(
name = "user-service_image",
base = "@distroless_java21",
tars = [
":deps_layer",
":app_layer",
],
entrypoint = ["/usr/bin/java", "-jar", "/app/user-service_deploy.jar"],
env = {
"JAVA_TOOL_OPTIONS": "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0",
},
)
# Create a tarball for docker load
oci_tarball(
name = "user-service_tar",
image = ":user-service_image",
repo_tags = ["user-service:latest"],
)
Quarkus Application Image
# services/order/BUILD.bazel
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball")
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
# Layer 1: Quarkus dependencies (rarely changes)
pkg_tar(
name = "quarkus_deps_layer",
srcs = [":order-service-quarkus-app/lib"],
package_dir = "/app/lib",
)
# Layer 2: Quarkus app (changes frequently)
pkg_tar(
name = "quarkus_app_layer",
srcs = [
":order-service-quarkus-app/quarkus-run.jar",
":order-service-quarkus-app/app",
],
package_dir = "/app",
)
# Layer 3: Native libraries (if using JNI)
pkg_tar(
name = "native_libs_layer",
srcs = glob(["src/main/resources/*.so"]),
package_dir = "/app/native",
)
oci_image(
name = "order-service_image",
base = "@ubi_minimal",
tars = [
":quarkus_deps_layer",
":quarkus_app_layer",
":native_libs_layer",
],
entrypoint = ["java", "-jar", "/app/quarkus-run.jar"],
env = {
"QUARKUS_HTTP_HOST": "0.0.0.0",
"QUARKUS_HTTP_PORT": "8080",
"JAVA_TOOL_OPTIONS": "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager",
},
exposed_ports = ["8080/tcp"],
)
oci_tarball(
name = "order-service_tar",
image = ":order-service_image",
repo_tags = [
"order-service:latest",
"order-service:v1.0.0",
],
)
Native Image Container
# services/payment/BUILD.bazel
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball")
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
# Native binary layer
pkg_tar(
name = "native_binary_layer",
srcs = [":payment-service-native"], # GraalVM native binary
package_dir = "/app",
)
# Native image is much smaller - use scratch or distroless
oci_image(
name = "payment-service_image",
base = "@distroless_static", # Static binary base (no libc needed)
tars = [":native_binary_layer"],
entrypoint = ["/app/payment-service-native"],
env = {
"QUARKUS_HTTP_HOST": "0.0.0.0",
"QUARKUS_HTTP_PORT": "8080",
},
exposed_ports = ["8080/tcp"],
)
oci_tarball(
name = "payment-service_tar",
image = ":payment-service_image",
repo_tags = ["payment-service:native"],
)
Multi-Architecture Images
Platform-Specific Image Build
# platforms/BUILD.bazel (from bazel-expert)
platform(
name = "linux_amd64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)
platform(
name = "linux_arm64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:arm64",
],
)
# services/user/BUILD.bazel
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_image_index")
# AMD64 image
oci_image(
name = "user-service_image_amd64",
base = "@distroless_java21_linux_amd64",
tars = [":app_layer"],
entrypoint = ["/usr/bin/java", "-jar", "/app/user-service.jar"],
)
# ARM64 image
oci_image(
name = "user-service_image_arm64",
base = "@distroless_java21_linux_arm64",
tars = [":app_layer"],
entrypoint = ["/usr/bin/java", "-jar", "/app/user-service.jar"],
)
# Multi-arch index (manifest list)
oci_image_index(
name = "user-service_image",
images = [
":user-service_image_amd64",
":user-service_image_arm64",
],
)
Building with Platform Flag
# Build for specific platform
bazel build //services/user:user-service_image_amd64 \
--platforms=//platforms:linux_amd64
# Build for ARM64
bazel build //services/user:user-service_image_arm64 \
--platforms=//platforms:linux_arm64
# Build multi-arch index
bazel build //services/user:user-service_image
Image Optimization
Layer Ordering (Most Stable First)
oci_image(
name = "optimized_image",
base = "@distroless_java21", # Layer 0: Base (rarely changes)
tars = [
":system_deps_layer", # Layer 1: System deps (rarely changes)
":shared_libs_layer", # Layer 2: Shared libraries (occasionally changes)
":app_deps_layer", # Layer 3: App dependencies (changes with version bumps)
":app_code_layer", # Layer 4: Application code (changes frequently)
":config_layer", # Layer 5: Config files (changes per environment)
],
)
JLink for Smaller Images
# Create custom JRE with only needed modules
load("@rules_java//java:defs.bzl", "java_runtime")
java_runtime(
name = "custom_jre",
srcs = [":jlink_output"], # Output of jlink command
java = "bin/java",
)
# Use custom JRE as base
oci_image(
name = "minimal_java_image",
base = ":custom_jre_image",
tars = [":app_layer"],
)
Spring Boot / Quarkus Layered JARs
# For Quarkus fast-jar layout
pkg_tar(
name = "quarkus_lib_layer",
srcs = glob(["quarkus-app/lib/main/*.jar"]),
package_dir = "/app/lib/main",
)
pkg_tar(
name = "quarkus_app_layer",
srcs = [
"quarkus-app/quarkus-run.jar",
"quarkus-app/app/*.jar",
],
package_dir = "/app",
)
oci_image(
name = "quarkus_fastjar_image",
base = "@distroless_java21",
tars = [
":quarkus_lib_layer",
":quarkus_app_layer",
],
entrypoint = ["java", "-jar", "/app/quarkus-run.jar"],
)
Pushing Images
Using oci_push
load("@rules_oci//oci:defs.bzl", "oci_push")
oci_push(
name = "push_user_service",
image = ":user-service_image",
remote_tags = ["latest", "v1.0.0", "{BUILD_TIMESTAMP}"],
repository = "ghcr.io/myorg/user-service",
)
# Push to registry
bazel run //services/user:push_user_service
# With authentication
CRANE_AUTH_USERNAME=$GITHUB_ACTOR \
CRANE_AUTH_PASSWORD=$GITHUB_TOKEN \
bazel run //services/user:push_user_service
Using crane (for advanced scenarios)
load("@rules_oci//oci:defs.bzl", "oci_tarball")
oci_tarball(
name = "user-service_for_crane",
image = ":user-service_image",
repo_tags = ["ghcr.io/myorg/user-service:latest"],
)
# Load and push with crane
crane push $(bazel cquery --output=files //services/user:user-service_for_crane) \
ghcr.io/myorg/user-service:latest
Image Inspection & Testing
Structure Test
load("@rules_oci//oci:defs.bzl", "oci_image")
# Use container_structure_test for validation
load("@container_structure_test//:defs.bzl", "container_structure_test")
container_structure_test(
name = "user-service_structure_test",
image = ":user-service_image",
configs = ["structure_test.yaml"],
)
# structure_test.yaml
schemaVersion: "2.0.0"
commandTests:
- name: "java exists"
command: "java"
args: ["-version"]
expectedError: ["openjdk version \"21"]
fileExistenceTests:
- name: "app jar exists"
path: "/app/user-service.jar"
shouldExist: true
fileContentTests:
- name: "correct entrypoint"
path: "/app/user-service.jar"
expectedContents: [".*"]
Image Size Check
# Check image size
bazel build //services/user:user-service_image
bazel run //tools:oci_inspect -- $(bazel cquery --output=files //services/user:user-service_image)
# Or use crane
crane manifest $(bazel cquery --output=files //services/user:user-service_image) | jq '.layers[] | .size' | awk '{sum+=$1} END {print sum/1024/1024 " MB"}'
Troubleshooting
"base image not found"
# Cause: oci.pull not properly configured in MODULE.bazel
# Fix: Ensure use_repo(oci, "image_name") is called
# Fix: Check digest is correct and image is accessible
"exec format error" in container
# Cause: Binary architecture doesn't match container architecture
# Fix: Build with correct --platforms flag
# Fix: Ensure base image matches target platform
Image too large
# Diagnosis: Check layer sizes
crane manifest ghcr.io/myorg/image:latest | jq '.layers[] | {size: .size, digest: .digest[:12]}'
# Fix: Separate deps and app code into different layers
# Fix: Use jlink to create minimal JRE
# Fix: Use distroless base instead of full OS
# Fix: Remove unnecessary files from layers
Push fails with authentication error
# Fix: Set CRANE_AUTH_USERNAME and CRANE_AUTH_PASSWORD
# Fix: Use docker login first if registry requires it
# Fix: Check repository URL is correct
Decision Trees
Choosing Base Image
Java application?
βββ Need glibc? (most Java apps)
β βββ Minimal size β gcr.io/distroless/java21-debian12
β βββ Need shell/debug β registry.access.redhat.com/ubi9/ubi-minimal
βββ Static binary (GraalVM native)
β βββ gcr.io/distroless/static (or scratch)
βββ Need package manager
βββ alpine:latest (not recommended for Java - musl issues)
Layer Strategy
How often does this change?
βββ Never (JDK, OS) β Base image
βββ Rarely (framework deps) β Separate layer
βββ Occasionally (app deps) β Separate layer
βββ Frequently (app code) β Top layer
βββ Per environment (config) β Topmost layer
References
Skill Interoperability
The bazel-oci-expert π³ skill provides containerization for:
- rules-quarkus π§: Containerize Quarkus applications built with Bazel.
- bazel-expert π: Core Bazel build infrastructure.
- bazel-k8s-expert βΈοΈ: Deploy containers to Kubernetes.
- quarkus-expert β‘: Quarkus-specific container configurations.
- graalvm-expert π: Native image container builds.