# Bazel Rules for apko

URL: https://edu.chainguard.dev/open-source/build-tools/apko/bazel-rules.md
Last Modified: March 12, 2026
Tags: apko, Procedural

Build secure, minimal Wolfi-based container images using Bazel

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_bazelrcThe files marked &ldquo;generated&rdquo; should be committed to your repository after generation. The files marked &ldquo;create manually&rdquo; 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 &amp;&amp; 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&rsquo;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 = &#34;my-apko-image&#34;, version = &#34;0.0.0&#34;, ) bazel_dep(name = &#34;rules_apko&#34;, version = &#34;1.5.37&#34;) # Set up the apko toolchain. # apko v1.1.12 is downloaded automatically — no separate installation needed. toolchain = use_extension(&#34;@rules_apko//apko:extensions.bzl&#34;, &#34;apko&#34;) toolchain.toolchain(apko_version = &#34;v1.1.12&#34;) use_repo(toolchain, &#34;apko_toolchains&#34;) register_toolchains(&#34;@apko_toolchains//:all&#34;) 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(&#34;@rules_apko//apko:defs.bzl&#34;, &#34;apko_bazelrc&#34;, &#34;apko_lock&#34;) # 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 = &#34;lock&#34;, config = &#34;apko.yaml&#34;, lockfile_name = &#34;apko.lock.json&#34;, ) 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 a 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_bazelrcYou 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-imageThis 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 = [&quot;my.repo.example.com&quot;]).
Step 7: Generate the lock file Run the lock target to generate apko.lock.json:
bazel run //:lockYou 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 keysThis 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 [&quot;v1.1.12&quot;, &quot;v1.1.12&quot;], 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 = &#34;my-apko-image&#34;, version = &#34;0.0.0&#34;, ) bazel_dep(name = &#34;rules_apko&#34;, version = &#34;1.5.37&#34;) # Set up the apko toolchain. # apko v1.1.12 is downloaded automatically — no separate installation needed. toolchain = use_extension(&#34;@rules_apko//apko:extensions.bzl&#34;, &#34;apko&#34;) toolchain.toolchain(apko_version = &#34;v1.1.12&#34;) use_repo(toolchain, &#34;apko_toolchains&#34;) register_toolchains(&#34;@apko_toolchains//:all&#34;) # Translate the lock file into Bazel repository targets. # Add this section after apko.lock.json has been generated. apk = use_extension(&#34;@rules_apko//apko:extensions.bzl&#34;, &#34;apko&#34;) apk.translate_lock( name = &#34;wolfi_base_lock&#34;, lock = &#34;//:apko.lock.json&#34;, ) use_repo(apk, &#34;wolfi_base_lock&#34;) Step 9: Update BUILD.bazel to add the image target Add the apko_image rule to your BUILD.bazel file:
load(&#34;@rules_apko//apko:defs.bzl&#34;, &#34;apko_bazelrc&#34;, &#34;apko_image&#34;, &#34;apko_lock&#34;) # 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 = &#34;lock&#34;, config = &#34;apko.yaml&#34;, lockfile_name = &#34;apko.lock.json&#34;, ) # Builds the container image from the lock file contents. # Run with: bazel build //:wolfi_base apko_image( name = &#34;wolfi_base&#34;, config = &#34;apko.yaml&#34;, contents = &#34;@wolfi_base_lock//:contents&#34;, tag = &#34;wolfi-base:latest&#34;, )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_baseYou 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 actionsThe 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&rsquo;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 //:lockThen 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 = &#34;my_image&#34;, config = &#34;apko.yaml&#34;, contents = &#34;@my_image_lock//:contents&#34;, tag = &#34;my-image:latest&#34;, )An example demonstrating usage with rules_oci:
apko_image( name = &#34;wolfi_base&#34;, config = &#34;apko.yaml&#34;, contents = &#34;@wolfi_base_lock//:contents&#34;, tag = &#34;wolfi-base:latest&#34;, ) oci_image( name = &#34;app&#34;, base = &#34;:wolfi_base&#34;, ) Attributes Name Description Type Mandatory Default name A unique name for this target. Name required architecture The CPU architecture this image should be built for. See apko architecture documentation. String optional &quot;&quot; args Additional arguments to pass to the apko build command. List of strings optional [] config Label to the apko.yaml configuration file. Label required contents Label to the contents repository generated by translate_lock. See Generating Configuration and the Lock File. Label required output Output format for the image. String optional &quot;oci&quot; tag Tag to apply to the resulting image. Only applicable when output is docker. String required 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 = &#34;lock&#34;, config = &#34;apko.yaml&#34;, lockfile_name = &#34;apko.lock.json&#34;, )Run with:
bazel run //:lock Parameters Name Description Default name Name of the target. required config Label to the apko.yaml configuration file. required lockfile_name Name 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_bazelrcThe 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 Name Description Default name Name of the target. &quot;apko_bazelrc&quot; repositories List of package repository hostnames to configure credential helpers for. [&quot;dl-cdn.alpinelinux.org&quot;, &quot;packages.wolfi.dev&quot;] kwargs Standard 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=&#34;basic:apk.cgr.dev:user:$(chainctl auth token --audience apk.cgr.dev)&#34;In your apko.yaml, reference your private repository using your Chainguard organization name:
contents: repositories: - https://apk.cgr.dev/$ORGANIZATION packages: - your-packageReplace $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&rsquo;s Private APK Repositories.

