The image_manifest, image_index, image_push, and image_load rules support Go templates for dynamic configuration. This feature enables flexible image naming and metadata based on build settings and Bazel's workspace status (stamping).
Templates allow you to:
- Use different registries/repositories for different environments (dev, staging, prod)
- Include version information from your build system
- Add git commit hashes or timestamps to tags and labels
- Create conditional logic for tag naming
- Inject dynamic metadata into container labels, environment variables, and annotations
First, create string flags using bazel_skylib:
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
string_flag(
name = "environment",
build_setting_default = "dev",
)
string_flag(
name = "region",
build_setting_default = "us-east-1",
)Reference the flags in your image_push rule:
load("@rules_img//img:push.bzl", "image_push")
image_push(
name = "push",
image = ":my_image",
# Use Go template syntax
registry = "{{.region}}.registry.example.com",
repository = "myapp/{{.environment}}",
tag_list = [
"latest",
"{{.environment}}-latest",
],
# Map build settings
build_settings = {
"environment": ":environment",
"region": ":region",
},
)# Use default values (dev, us-east-1)
bazel run //:push
# Override for production
bazel run //:push --//:environment=prod --//:region=eu-west-1This would push to:
- Registry:
eu-west-1.registry.example.com - Repository:
myapp/prod - Tags:
latest,prod-latest
Stamping allows you to include dynamic build information like git commits, timestamps, and version numbers in your container tags, labels, environment variables, and annotations.
Important: Stamping requires explicit opt-in at two levels:
-
Bazel level: Enable stamping with the
--stampflag- By default, Bazel disables stamping for build reproducibility and performance
- You must explicitly add
--stampto your build command or.bazelrc
-
Target level: Enable stamping for specific
image_push,image_load,image_manifest, orimage_indextargets- Set
stamp = "enabled"on the target, OR - Set
stamp = "auto"(the default) and use--@rules_img//img/settings:stamp=enabled
- Set
Both levels must be enabled for stamping to work. If either is disabled, stamp variables will not be available in templates.
Create a script that outputs key-value pairs:
#!/usr/bin/env bash
# File: workspace_status.sh
# Variables prefixed with STABLE_ are included in the cache key.
# If their value changes, the target must be rebuilt.
# Only use for values that rarely update for better performance.
echo "STABLE_CONTAINER_VERSION_TAG v1.2.3"
# Variables without STABLE_ prefix are volatile.
# These variables are not included in the cache key.
# If their values changes, a target may still include
# a stale value from a previous build.
echo "BUILD_TIMESTAMP $(date +%s)"
echo "GIT_COMMIT $(git rev-parse HEAD 2>/dev/null || echo 'unknown')"
echo "GIT_BRANCH $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')"
echo "GIT_DIRTY $(if git diff --quiet 2>/dev/null; then echo 'clean'; else echo 'dirty'; fi)"Make it executable:
chmod +x workspace_status.shAdd to .bazelrc:
# Configure workspace status script
build --workspace_status_command=./workspace_status.sh| Value | Behavior |
|---|---|
"enabled" |
Always use stamp values (if Bazel --stamp is set) |
"disabled" |
Never use stamp values |
"auto" |
Use the --@rules_img//img/settings:stamp=enabled flag (default) |
Stamp variables are empty or not replaced:
- Check that
--stampis set at Bazel level - Check that
stamp = "enabled"or proper flags are set - Verify workspace_status_command is executable and in .bazelrc
- Test your script:
./workspace_status.shshould output key-value pairs
Build not reproducible:
- Use
stamp = "disabled"for development builds - Only enable stamping for release builds using configs
Testing stamp values:
# Check what values are available
bazel build --stamp //:push
cat bazel-bin/push_template.json # Shows template with stamp placeholders
cat bazel-bin/push.json # Shows expanded valuestag_list = [
# Use version tag if available, otherwise "dev"
"{{if .STABLE_CONTAINER_VERSION_TAG}}{{.STABLE_CONTAINER_VERSION_TAG}}{{else}}dev{{end}}",
# Add suffix only in dev environment
"latest{{if eq .environment \"dev\"}}-dev{{end}}",
# Complex conditions
"{{if and .GIT_COMMIT (ne .GIT_BRANCH \"main\")}}{{.GIT_BRANCH}}-{{.GIT_COMMIT}}{{end}}",
]You can use both build settings and stamp values together:
image_push(
name = "push",
image = ":my_image",
# Combine region from build setting with stamp info
registry = "{{.region}}.registry.example.com",
repository = "{{.organization}}/{{.STABLE_BUILD_USER}}/myapp",
tag_list = [
"{{.environment}}-{{.STABLE_CONTAINER_VERSION_TAG}}",
"{{.environment}}-{{.GIT_COMMIT}}",
],
build_settings = {
"environment": ":environment",
"region": ":region",
"organization": ":organization",
},
stamp = "enabled",
)The image_manifest rule automatically provides access to the base image's configuration and manifest through template variables. This allows you to reference or extend metadata from the base image.
When using a base image in image_manifest, the following template variables are available:
$<ENV>- An environment variable from the base image (like$PATH).base.config- The base image's OCI configuration JSON.base.manifest- The base image's OCI manifest JSON
Important: All JSON field names are automatically converted to lowercase for case-insensitive access. For example, if the parent config has "Architecture": "amd64", you access it as .base.config.architecture.
image_manifest(
name = "my_image",
base = "@alpine",
layers = [":app_layer"],
labels = {
"base.architecture": "{{.base.config.architecture}}", # e.g., "amd64"
"base.os": "{{.base.config.os}}", # e.g., "linux"
},
)The parent image's environment variables are stored as an array of "KEY=VALUE" strings in .base.config.config.env. Use the getkv, appendkv, or prependkv functions to work with them:
image_manifest(
name = "my_image",
base = "@distroless_base",
layers = [":app_layer"],
env = {
# Extend PATH from parent by appending a custom directory
# If the base sets $PATH to "/usr/bin:/bin", this results in "/usr/bin:/bin:/opt/bin"
"PATH": "{{ $PATH }}:/opt/bin",
# Extend PYTHONPATH from parent by appending a custom directory
# If the base sets $PYTHONPATH to "/usr/lib/python/site-packages", this results in "/usr/lib/python/site-packages:/custom/site-packages"
"PYTHONPATH": """{{appendkv .base.config.config.env "PYTHONPATH" ":/custom/site-packages"}}""",
# Or prepend to LD_LIBRARY_PATH
# If the base sets $LD_LIBRARY_PATH to "/usr/lib", this results in "/opt/lib:/usr/lib"
"LD_LIBRARY_PATH": """{{prependkv .base.config.config.env "LD_LIBRARY_PATH" "/opt/lib:"}}""",
# Access other parent env vars
"PARENT_HOME": """{{getkv .base.config.config.env "HOME"}}""",
},
)image_manifest(
name = "my_image",
base = "@alpine",
layers = [":app_layer"],
labels = {
# Note: lowercase field names
"base.user": "{{.base.config.config.user}}",
},
)image_manifest(
name = "my_image",
base = "@alpine",
layers = [":app_layer"],
annotations = {
"parent.digest": "{{.base.manifest.config.digest}}",
"parent.mediatype": "{{.base.manifest.mediatype}}",
},
)The image_index rule also provides access to the first manifest's data through .base.config and .base.manifest:
image_index(
name = "multiarch",
manifests = [":image_amd64", ":image_arm64"],
annotations = {
# Accesses the first manifest's config
"reference.architecture": "{{.base.config.architecture}}",
},
)In addition to standard Go template features, several custom functions are available to help work with OCI image metadata:
OCI image configurations store environment variables and other settings as arrays of "KEY=VALUE" strings. These functions help extract and manipulate such values:
Extracts a value from a key-value array.
Syntax: getkv <array> <key>
env = {
# Extract PATH from parent's env array
"ORIGINAL_PATH": """{{getkv .base.config.config.env "PATH"}}""",
}Example: If the parent has ["PATH=/usr/bin", "HOME=/root"], then getkv .base.config.config.env "PATH" returns /usr/bin.
Extracts a value from a key-value array and appends a suffix.
Syntax: appendkv <array> <key> <suffix>
env = {
# Extend PATH by appending a custom directory
"PATH": """{{appendkv .base.config.config.env "PATH" ":/custom/bin"}}""",
}Example: If parent has PATH=/usr/bin, this returns /usr/bin:/custom/bin.
If the key doesn't exist in the array, returns just the suffix.
Extracts a value from a key-value array and prepends a prefix.
Syntax: prependkv <array> <key> <prefix>
env = {
# Extend LD_LIBRARY_PATH by prepending
"LD_LIBRARY_PATH": """{{prependkv .base.config.config.env "LD_LIBRARY_PATH" "/opt/lib:"}}""",
}Example: If parent has LD_LIBRARY_PATH=/usr/lib, this returns /opt/lib:/usr/lib.
If the key doesn't exist in the array, returns just the prefix.
Splits a string by a separator.
Syntax: split <string> <separator>
# Split PATH into components (returns a slice)
{{range split .path ":"}}{{.}}{{end}}Joins a slice of strings with a separator.
Syntax: join <slice> <separator>
# Join slice with colons
{{join .components ":"}}Check if a string has a given prefix or suffix.
Syntax: hasprefix <string> <prefix> or hassuffix <string> <suffix>
{{if hasprefix .path "/usr"}}starts with /usr{{end}}
{{if hassuffix .tag "-dev"}}is a dev tag{{end}}Remove a prefix or suffix from a string.
Syntax: trimprefix <string> <prefix> or trimsuffix <string> <suffix>
# Remove /usr prefix
{{trimprefix .path "/usr"}}
# Remove -dev suffix
{{trimsuffix .tag "-dev"}}