Getting Started with apko
Quickstart to get apko up and running
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.
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
apkoseparately.rules_apkomanages its own hermeticapkotoolchain and automatically downloadsapko v1.1.12on first build.
Before you begin, ensure you have the following:
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.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 “generated” should be committed to your repository after generation. The files marked “create manually” are written by you as part of this guide.
Create a new directory for your project and change into it:
mkdir my-apko-image && cd my-apko-imageCreate a root .bazelrc file in your project directory. This file does two
things:
# 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-cacheNote: The try-import directive tells Bazel to load
.apko/.bazelrcif it exists. This file is generated in a later step by runningbazel run //:apko_bazelrc. Until that step is complete the directive is safely ignored.
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")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_imageis not included at this stage is that theapko_imagerule checks forapko.lock.jsonat Bazel load time — before any targets are run. Since the lock file does not exist yet, including apko_imageat this stage would cause a load-time error. Once the lock file exists you will addapko_imagein a later step.
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_64With your project files in place, run the following two commands in order.
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 requestsBoth 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_bazelrcconfigures credential helpers fordl-cdn.alpinelinux.organd packages.wolfi.dev. If you are using additional repositories, pass them to the repositories attribute:apko_bazelrc(repositories = ["my.repo.example.com"]).
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
DEBUGmessageapko toolchain apko has multiple versions ["v1.1.12", "v1.1.12"], selected v1.1.12may appear in your output. This is normal and expected — it occurs because both the toolchain setup and the lock translation inMODULE.bazelreference the sameapkoextension. It does not indicate a problem.
Now that the lock file exists, update your project files to add the image build target.
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")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.
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
INFOmessages 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 —apkodeduplicates 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.
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_baseapko_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",
)| 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 | "" |
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 | "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.
apko_lock(
name = "lock",
config = "apko.yaml",
lockfile_name = "apko.lock.json",
)Run with:
bazel run //:lock| 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| 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 |
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-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’s Private APK Repositories.
Last updated: 2026-03-12 00:00