Assembling a Container Image with apko
Originally published in Japanese at https://zenn.dev/ymotongpoo/books/chainguard-image-toolchain/viewer/30-apko-handson.
Chapter 5 built the hello APK package. This chapter combines that APK with Wolfi’s published packages to assemble an actual, runnable container image.
Writing apko.yaml
Create apko.yaml in your working directory with the following content:
contents:
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
- melange.rsa.pub
repositories:
- https://packages.wolfi.dev/os
- "@local ./packages"
packages:
- wolfi-baselayout
- hello@local
entrypoint:
command: /usr/bin/hello
accounts:
run-as: 65532
archs:
- x86_64
repositories lists both Wolfi’s public repository and the local ./packages directory that melange produced in Chapter 5. The local repository is given the name @local, and the packages field specifies packages using that named repository explicitly, as in hello@local. keyring lists both Wolfi’s official signing key and the melange.rsa.pub you generated yourself in Chapter 5, since your locally built APK is subject to signature verification just like Wolfi’s published packages.
Specifying a UID in accounts.run-as makes the container run as that UID instead of root. Many Chainguard Images likewise set a non-root default runtime user.
Building the image
apko is also distributed as a Docker image, cgr.dev/chainguard/apko. This command writes out an OCI image as a tarball:
docker run --rm -v "${PWD}":/work cgr.dev/chainguard/apko \
build apko.yaml hello:latest hello.tar
The arguments, in order, are the config file, the tag to give the image, and the output tarball’s filename. The resulting hello.tar can be loaded into your local Docker daemon with docker load. Because apko always emits an OCI Image Index even when only a single architecture is specified, docker load appends the architecture name to the tag — here it loads as hello:latest-amd64.
docker load < hello.tar
docker run --rm hello:latest-amd64
Running it prints GNU Hello’s standard greeting. You’ve gotten here without writing a single Dockerfile — purely from declarations.
Publishing directly to a registry
apko build goes through a local tarball, but in a CI environment you’ll often want to push directly to a registry without going through the Docker daemon. That’s what apko publish is for:
docker run --rm -v "${PWD}":/work cgr.dev/chainguard/apko \
publish apko.yaml registry.example.com/hello:latest
publish sends the OCI image directly through the registry’s API, so building and pushing complete in a single command.
apko publish itself has no dedicated flag like --username for credentials — it simply uses go-containerregistry’s standard authn.DefaultKeychain mechanism. This reads the ~/.docker/config.json file that docker login writes (or each cloud’s credential helper), and apko’s README likewise notes that it “assumes you’ve already stored credentials in the keychain via docker login.” When pushing to ghcr.io, if the GITHUB_TOKEN environment variable is set inside GitHub Actions, authentication happens automatically, without a docker login step.
Checking the SBOM
apko generates an SBOM as part of the build. You can specify where to write it at build time with an option like --sbom-path, or extract it from an already-built image to inspect it. As explained in Chapter 6, this SBOM carries forward the SBOM fragment melange embedded in the hello package back in Chapter 5. If you actually open the SBOM’s contents with something like jq, you’ll find the hello package’s version and license information recorded right alongside the other, Wolfi-derived packages.
Looking back at the difference from Dockerfiles
Looking back at everything so far, the difference from Dockerfiles boils down to three points:
- Because package installation is pinned to
apk’s dependency resolution, the version that gets resolved on any given build follows the declaration (or a lockfile), even though the repository index is consulted every time - Because there’s no way to run an arbitrary command, everything in the image can be explained purely by the combination of APK packages
- Because the SBOM is composed from information internal to the build process, it’s more accurate than an after-the-fact scan
The next chapter looks at how melange and apko work together, focusing on how the SBOM handoff and reproducibility relate.