Bazel Rules for apko

Build secure, minimal Wolfi-based container images using Bazel
  10 min read

rules_apko is an open source plugin for Bazel that makes it possible to build secure, minimal Wolfi-based container images using the Bazel build system. It wraps the apko tool for use under Bazel, providing hermetic, reproducible image builds with full Bazel caching support.

By the end of this guide you will have a working Bazel project that builds a minimal Wolfi-based container image using rules_apko.

How to build a container with Bazel using rules_apko

This page covers rules_apko version 1.5.37 with Bazel 9.0.1 using Bzlmod, which is the only supported dependency management method in Bazel 9. If you are on an earlier version of Bazel, you should upgrade to Bazel 9 before following this guide.

Note: You do not need to install apko separately. rules_apko manages its own hermetic apko toolchain and automatically downloads apko v1.1.12 on first build.

Prerequisites

Before you begin, ensure you have the following:

  • Bazel 9.x installed. This page was written and tested against Bazel 9.0.1. Follow the Bazel installation guide for details. Note the Bazel version in use by confirming the dev.chainguard.package.main label on your image is bazel-9.
  • chainctl installed and authenticated. chainctl is the Chainguard command line tool. See the chainctl documentation for installation instructions.
  • rules_apko version 1.5.37 available in the Bazel Central Registry. No separate download is required — Bazel fetches it automatically when you declare it in MODULE.bazel.

Project Structure

A complete rules_apko project requires the following files:

my-apko-image/
├── .bazelrc                 ← create manually
├── MODULE.bazel             ← create manually, updated in two stages
├── BUILD.bazel              ← create manually, updated in two stages
├── apko.yaml                ← create manually
├── apko.lock.json           ← generated by bazel run //:lock
├── MODULE.bazel.lock        ← auto-generated by Bazel, do not edit
└── .apko/
    ├── .bazelrc             ← generated by bazel run //:apko_bazelrc
    └── range.sh             ← generated by bazel run //:apko_bazelrc

The files marked “generated” should be committed to your repository after generation. The files marked “create manually” are written by you as part of this guide.

Setup

Step 1: Create your project directory

Create a new directory for your project and change into it:

mkdir my-apko-image && cd my-apko-image

Step 2: Create the root .bazelrc file

Create a root .bazelrc file in your project directory. This file does two things:

  • It activates the apko credential helpers for partial HTTP range requests (once they are generated in a later step)
  • It keeps the Bazel repository cache outside your project directory, which is required by Bazel 9:
# Import apko credential helper configuration for partial package fetches
try-import .apko/.bazelrc

# Keep Bazel repo cache outside the project directory (required by Bazel 9)
build --repo_contents_cache=/tmp/bazel-cache

Note: The try-import directive tells Bazel to load .apko/.bazelrc if it exists. This file is generated in a later step by running bazel run //:apko_bazelrc. Until that step is complete the directive is safely ignored.

Step 3: Create the Stage 1 MODULE.bazel file

Create a MODULE.bazel file in your project directory. This file declares your project’s external dependencies and sets up the apko toolchain. At this stage it does not yet include the lock file translation — that is added after the lock file is generated:

module(
    name = "my-apko-image",
    version = "0.0.0",
)

bazel_dep(name = "rules_apko", version = "1.5.37")

# Set up the apko toolchain.
# apko v1.1.12 is downloaded automatically — no separate installation needed.
toolchain = use_extension("@rules_apko//apko:extensions.bzl", "apko")
toolchain.toolchain(apko_version = "v1.1.12")
use_repo(toolchain, "apko_toolchains")
register_toolchains("@apko_toolchains//:all")

Step 4: Create the Stage 1 BUILD.bazel file

Create a BUILD.bazel file in your project directory. At this stage it contains only the apko_bazelrc and apko_lock rules. The apko_image rule is added after the lock file is generated:

load("@rules_apko//apko:defs.bzl", "apko_bazelrc", "apko_lock")

# Generates .apko/.bazelrc and .apko/range.sh for partial package fetches.
# Run with: bazel run //:apko_bazelrc
apko_bazelrc()

# Generates the lock file that pins all package versions and checksums.
# Run with: bazel run //:lock
apko_lock(
    name = "lock",
    config = "apko.yaml",
    lockfile_name = "apko.lock.json",
)

Note: The reason apko_image is not included at this stage is that the apko_image rule checks for apko.lock.json at Bazel load time — before any targets are run. Since the lock file does not exist yet, including apko_image at this stage would cause a load-time error. Once the lock file exists you will add apko_image in a later step.

Step 5: Create the apko.yaml configuration file

Create an apko.yaml file in your project directory. This file defines the container image — its base packages, repository, signing key, and target architectures. The following example builds a minimal Wolfi-based image:

contents:
  keyring:
    - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
  repositories:
    - https://packages.wolfi.dev/os
  packages:
    - wolfi-base

entrypoint:
  command: /bin/sh

archs:
  - aarch64
  - x86_64

Generating configuration and the lock file

With your project files in place, run the following two commands in order.

Step 6: Generate the credential helper configuration

Run the apko_bazelrc target to generate .apko/.bazelrc and .apko/range.sh:

bazel run //:apko_bazelrc

You will see output similar to the following:

INFO: Analyzed target //:apko_bazelrc (6 packages loaded, 14 targets configured).
INFO: Found 1 target...
Target //:apko_bazelrc up-to-date:
  bazel-bin/apko_bazelrc_update.sh
INFO: Build completed successfully, 2 total actions
INFO: Running command line: bazel-bin/apko_bazelrc_update.sh
Copying file .../range.sh to .apko/range.sh in /home/user/my-apko-image
Copying file .../apko_bazelrc_bazelrc to .apko/.bazelrc in /home/user/my-apko-image

This generates two files in the .apko/ subdirectory:

  • .apko/.bazelrc — configures Bazel credential helpers for the Wolfi and Alpine package repositories, enabling partial HTTP range requests so Bazel fetches only the specific byte ranges of APK packages it needs
  • .apko/range.sh — the credential helper script used by Bazel when making range requests

Both files should be committed to your repository. They are activated by the try-import .apko/.bazelrc directive in your root .bazelrc file.

Note: By default, apko_bazelrc configures credential helpers for dl-cdn.alpinelinux.org and packages.wolfi.dev. If you are using additional repositories, pass them to the repositories attribute: apko_bazelrc(repositories = ["my.repo.example.com"]).

Step 7: Generate the lock file

Run the lock target to generate apko.lock.json:

bazel run //:lock

You will see output similar to the following:

INFO: Analyzed target //:lock (84 packages loaded, 487 targets configured).
INFO: Found 1 target...
Target //:lock up-to-date:
  bazel-bin/_lock_run.sh
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/_lock_run.sh
2026/03/12 13:07:06 INFO Determining packages for 2 architectures: [arm64 amd64]
2026/03/12 13:07:06 INFO Discovered 0 auto-discovered keys

This generates apko.lock.json in your project directory. The lock file pins the exact versions and checksums of all packages required to build your image for each target architecture. Commit this file to your repository to ensure reproducible builds.

Note: The DEBUG message apko toolchain apko has multiple versions ["v1.1.12", "v1.1.12"], selected v1.1.12 may appear in your output. This is normal and expected — it occurs because both the toolchain setup and the lock translation in MODULE.bazel reference the same apko extension. It does not indicate a problem.

Building the image

Now that the lock file exists, update your project files to add the image build target.

Step 8: Update MODULE.bazel to add lock file translation

Add the translate_lock extension call to the end of your MODULE.bazel file:

module(
    name = "my-apko-image",
    version = "0.0.0",
)

bazel_dep(name = "rules_apko", version = "1.5.37")

# Set up the apko toolchain.
# apko v1.1.12 is downloaded automatically — no separate installation needed.
toolchain = use_extension("@rules_apko//apko:extensions.bzl", "apko")
toolchain.toolchain(apko_version = "v1.1.12")
use_repo(toolchain, "apko_toolchains")
register_toolchains("@apko_toolchains//:all")

# Translate the lock file into Bazel repository targets.
# Add this section after apko.lock.json has been generated.
apk = use_extension("@rules_apko//apko:extensions.bzl", "apko")
apk.translate_lock(
    name = "wolfi_base_lock",
    lock = "//:apko.lock.json",
)
use_repo(apk, "wolfi_base_lock")

Step 9: Update BUILD.bazel to add the image target

Add the apko_image rule to your BUILD.bazel file:

load("@rules_apko//apko:defs.bzl", "apko_bazelrc", "apko_image", "apko_lock")

# Generates .apko/.bazelrc and .apko/range.sh for partial package fetches.
# Run with: bazel run //:apko_bazelrc
apko_bazelrc()

# Generates the lock file that pins all package versions and checksums.
# Run with: bazel run //:lock
apko_lock(
    name = "lock",
    config = "apko.yaml",
    lockfile_name = "apko.lock.json",
)

# Builds the container image from the lock file contents.
# Run with: bazel build //:wolfi_base
apko_image(
    name = "wolfi_base",
    config = "apko.yaml",
    contents = "@wolfi_base_lock//:contents",
    tag = "wolfi-base:latest",
)

The contents attribute references @wolfi_base_lock//:contents — this is the Bazel repository generated by the translate_lock call in MODULE.bazel. The name wolfi_base_lock in both files must match.

Step 10: Build the Image

Run the build:

bazel build //:wolfi_base

You will see output similar to the following:

INFO: Analyzed target //:wolfi_base (123 packages loaded, 656 targets configured).
INFO: From Action wolfi_base:
2026/03/12 12:46:27 INFO installing wolfi-keys (1-r13) arch=aarch64
2026/03/12 12:46:27 INFO installing wolfi-baselayout (20230201-r28) arch=aarch64
2026/03/12 12:46:27 INFO installing wolfi-keys (1-r13) arch=x86_64
2026/03/12 12:46:27 INFO installing wolfi-baselayout (20230201-r28) arch=x86_64
2026/03/12 12:46:27 INFO installing ca-certificates-bundle (20251003-r4) arch=x86_64
...
2026/03/12 12:46:27 INFO installing wolfi-base (1-r7) arch=x86_64
2026/03/12 12:46:27 INFO layer digest: sha256:44cc053506b4e236f7e32026147836ce082fa58d0a329ff2aab1bb61d0c6bcfc arch=x86_64
INFO: Found 1 target...
Target //:wolfi_base up-to-date:
  bazel-bin/wolfi_base
INFO: Build completed successfully, 128 total actions

The built image is available at bazel-bin/wolfi_base.

Note: You may see INFO messages about duplicate package IDs in the SBOM during the build, for example: INFO duplicate package ID found in SBOM, deduplicating package... These are normal and expected — apko deduplicates packages that appear multiple times in the dependency graph when generating the Software Bill of Materials (SBOM). They do not indicate a problem with your build.

On subsequent builds, Bazel’s caching means most actions will be retrieved from cache rather than rebuilt. You will see output like 127 action cache hit, 1 internal — this is expected and demonstrates one of the key benefits of building images with Bazel.

Updating the Lock File

When you update apko.yaml to add, remove, or change packages, regenerate the lock file by running:

bazel run //:lock

Then rebuild the image:

bazel build //:wolfi_base

Rules Reference

apko_image

Builds an OCI container image from APK packages using an apko.yaml configuration file and a pre-generated lock file.

apko_image(
    name = "my_image",
    config = "apko.yaml",
    contents = "@my_image_lock//:contents",
    tag = "my-image:latest",
)

An example demonstrating usage with rules_oci:

apko_image(
    name = "wolfi_base",
    config = "apko.yaml",
    contents = "@wolfi_base_lock//:contents",
    tag = "wolfi-base:latest",
)

oci_image(
    name = "app",
    base = ":wolfi_base",
)

Attributes

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
architectureThe CPU architecture this image should be built for. See apko architecture documentation.Stringoptional""
argsAdditional arguments to pass to the apko build command.List of stringsoptional[]
configLabel to the apko.yaml configuration file.Labelrequired
contentsLabel to the contents repository generated by translate_lock. See Generating Configuration and the Lock File.Labelrequired
outputOutput format for the image.Stringoptional"oci"
tagTag to apply to the resulting image. Only applicable when output is docker.Stringrequired

apko_lock

Generates a lock file that pins the exact versions and checksums of all packages required to build your image. The lock file is written directly into your project directory and should be committed to your repository.

apko_lock(
    name = "lock",
    config = "apko.yaml",
    lockfile_name = "apko.lock.json",
)

Run with:

bazel run //:lock

Parameters

NameDescriptionDefault
nameName of the target.required
configLabel to the apko.yaml configuration file.required
lockfile_nameName of the generated lock file.required

apko_bazelrc

Generates .apko/.bazelrc and .apko/range.sh to enable partial HTTP range requests when fetching APK packages. This significantly reduces download size by fetching only the byte ranges of each package that Bazel needs.

apko_bazelrc()

Run with:

bazel run //:apko_bazelrc

The generated .apko/.bazelrc file configures Bazel credential helpers for the specified repositories. Activate it by adding the following to your root .bazelrc:

try-import .apko/.bazelrc

Parameters

NameDescriptionDefault
nameName of the target."apko_bazelrc"
repositoriesList of package repository hostnames to configure credential helpers for.["dl-cdn.alpinelinux.org", "packages.wolfi.dev"]
kwargsStandard Bazel attributes such as tags and testonly.none

Using Private APK Repositories

If you are using a private Chainguard APK repository, you need to provide your Chainguard token to the apko runtime via the HTTP_AUTH environment variable. Set it before running any bazel run or bazel build commands:

export HTTP_AUTH="basic:apk.cgr.dev:user:$(chainctl auth token --audience apk.cgr.dev)"

In your apko.yaml, reference your private repository using your Chainguard organization name:

contents:
  repositories:
    - https://apk.cgr.dev/$ORGANIZATION
  packages:
    - your-package

Replace $ORGANIZATION with your Chainguard organization name. For full details on setting up and using private APK repositories with Chainguard, including how to configure your organization and authenticate, see Chainguard’s Private APK Repositories.

Last updated: 2026-03-12 00:00