# Bazel rules for apko

URL: https://chainguard-docs-preview-git-fix-ai-metadata-year.chainguard.app/open-source/build-tools/apko/bazel-rules.md
Last Modified: September 28, 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](https://github.com/chainguard-dev/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](https://bazel.build/install)
  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](/platform/chainctl/) for
  installation instructions.
- **`rules_apko` version `1.5.37`** available in the
  [Bazel Central Registry](https://registry.bazel.build/modules/rules_apko).
  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:

```shell
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:

```shell
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:

```shell
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 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:

```yaml
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`:

```shell
bazel run //:apko_bazelrc
```

You will see output similar to the following:

```shell
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:

```shell
bazel run //:lock
```

You will see output similar to the following:

```shell
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:

```shell
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:

```shell
bazel build //:wolfi_base
```

You will see output similar to the following:

```shell
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:

```shell
bazel run //:lock
```

Then rebuild the image:

```shell
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.

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

An example demonstrating usage with `rules_oci`:

```shell
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

| Name | Description | Type | Mandatory | Default |
| :--- | :--- | :--- | :--- | :--- |
| `name` | A unique name for this target. | Name | required | |
| `architecture` | The CPU architecture this image should be built for. Refer to [apko architecture documentation](https://github.com/chainguard-dev/apko/blob/main/docs/apko_file.md#archs-top-level-element). | String | optional | `""` |
| `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`. Refer to [Generating Configuration and the Lock File](#generating-configuration-and-the-lock-file). | Label | required | |
| `output` | Output format for the image. | String | optional | `"oci"` |
| `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.

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

Run with:

```shell
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.

```shell
apko_bazelrc()
```

Run with:

```shell
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`:

```shell
try-import .apko/.bazelrc
```

#### Parameters

| Name | Description | Default |
| :--- | :--- | :--- |
| `name` | Name of the target. | `"apko_bazelrc"` |
| `repositories` | List of package repository hostnames to configure credential helpers for. | `["dl-cdn.alpinelinux.org", "packages.wolfi.dev"]` |
| `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:

```shell
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:

```yaml
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](/chainguard/containers/building-and-modifying/packages/private-apk-repos/).

