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.
Stamping behavior is controlled at two levels:
-
Global setting (
--@rules_img//img/settings:stamp): Controls the default behavior for all targets"auto"(default): Stamps when Bazel's--stampflag is set and templates contain{{}}placeholders"force": Always stamps if templates contain{{}}placeholders, ignoring Bazel's--stampflag"disabled": Never stamps
-
Target level (
stampattribute): Overrides the global setting for a specific target"auto"(default): Defers to the global setting"force": Always stamp if templates contain{{}}placeholders, ignoring Bazel's--stampflag"disabled": Never include stamp information
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 |
|---|---|
"auto" |
(default) Defer to global setting. With global default "auto", stamps when --stamp is set and templates contain {{}} |
"force" |
Always stamp if templates contain {{}} placeholders, regardless of --stamp |
"disabled" |
Never use stamp values |
Stamp variables are empty or not replaced:
- Check that
--stampis set at Bazel level (or usestamp = "force"to bypass) - Check that
stamp = "force"or--@rules_img//img/settings:stamp=forceis set - Verify workspace_status_command is executable and in .bazelrc
- Test your script:
./workspace_status.shshould output key-value pairs
Build not reproducible:
- Set
--nostampand use the defaultautosetting for rules_img stamping - Use
stamp = "disabled"if you never want to include stamping information - 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 = "force",
)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"}}