Chainguard Images in Practice

Originally published in Japanese at https://zenn.dev/ymotongpoo/books/chainguard-image-toolchain/viewer/60-chainguard-images-practice.

The chapters so far have run melange and apko individually. This chapter checks how the two tools come together in Chainguard’s own image distribution.

The build is really just a Makefile and CLI calls

If you actually look at the wolfi-dev/os repository that manages Wolfi’s package definitions, what drives the build is a Makefile sitting right at the repository root. The build proceeds by calling the melange and apko command-line tools directly — there’s no Bazel BUILD file or WORKSPACE file anywhere. There’s no GitHub Actions workflow definition under .github/ either — just trust configuration that lets Chainguard’s internal automation tooling write to this repository over OIDC. In other words, this doesn’t depend on Bazel, and it doesn’t depend on a public GitHub Actions workflow either.

In other words, the procedure we’ve confirmed throughout this book — write a melange.yaml and run melange build, write an apko.yaml and run apko build (or apko publish) — isn’t a simplified stand-in. It’s essentially what Chainguard actually does. Scale aside, the skeleton of the pipeline is the same one you exercised by hand in Chapters 5 and 7.

Applying this to your own Go application

So far we’ve used GNU Hello, an existing C program, as our subject. Let’s consider applying this to your own application instead. A typical Go application’s Dockerfile is usually written as a multi-stage build like this:

FROM golang:1.23 AS builder
WORKDIR /src
COPY . .
RUN go build -o /out/app .

FROM scratch
COPY --from=builder /out/app /app
ENTRYPOINT ["/app"]

Replacing this with melange and apko, melange’s go/build action takes over the build stage, and apko takes over everything after FROM scratch. Building on melange’s examples/go-build.yaml, melange.yaml looks like this:

package:
  name: hello-go
  version: 0.0.1
  epoch: 0
  description: "A project that will greet the world infinitely"

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

pipeline:
  - uses: git-checkout
    with:
      repository: https://github.com/puerco/hello.git
      expected-commit: a73c4feb284dc6ed1e5758740f717f99dcd4c9d7
      tag: v${{package.version}}

  - uses: go/build
    with:
      packages: .
      output: hello-go

git-checkout fetches the source, and you just hand go/build the package to build (packages: .) and the binary name to output (output: hello-go). Everything a Dockerfile’s RUN go build would do is concentrated into this single go/build action. The build command is the same as in Chapter 5:

docker run --privileged --rm -v "${PWD}":/work \
  cgr.dev/chainguard/melange build hello-go.yaml \
  --arch x86_64 --signing-key melange.rsa

Assemble the resulting hello-go APK package into an image with apko. apko.yaml looks almost identical to the Chapter 7 example:

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-go@local

entrypoint:
  command: /usr/bin/hello-go

accounts:
  run-as: 65532

archs:
  - x86_64

The Dockerfile’s second half, which loaded only a minimal set of files starting from FROM scratch, is replaced by a contents.packages that lists only wolfi-baselayout and your own binary. Go binaries are usually statically linked, so there’s almost never a need to specify individual runtime libraries. From here, the same apko build (or apko publish) as Chapter 7 finishes the image, without writing a single line of Dockerfile.

The mechanism behind rebuilding from source every day

Chainguard Images (the set of images published under the chainguard-images organization) are operated so that they’re rebuilt from source every day, starting from Wolfi’s package definitions and apko configuration files. As we confirmed in Chapter 3, every package in Wolfi is built with melange, so when a vulnerability fix lands upstream, updating Wolfi’s package definition and rebuilding with melange is all it takes to get an APK package that reflects the fix. apko just recomposes the image from that set of APK packages, so every build keeps distributing an image with fewer CVEs, reflecting the latest package state.

This works because it’s the flip side of the problem we confirmed in Chapter 2. In a Dockerfile-based approach, it was hard to mechanically tell from outside how a base image’s contents were put together, and accurately tracking vulnerabilities meant relying on scanning. With melange and apko together, an image’s contents are declared as a list of APK packages, and the SBOM is also generated from information internal to the build process, so you can always accurately tell exactly which version of which package is included.

Options for using this in a Bazel monorepo

As we’ve seen, Bazel doesn’t appear anywhere in Chainguard’s own build pipeline. That said, for teams that manage their own applications in a Bazel monorepo and want to pull in Chainguard Images as a base image, Chainguard publishes a set of Bazel rules called rules_apko. This is a wrapper that integrates running apko into Bazel’s build graph — an integration layer built for external users of Bazel, separate from Chainguard’s own internal build. For readers who don’t manage their applications in a Bazel monorepo, this falls outside the scope of this book.

Surveying the whole ecosystem

To recap the relationship among the pieces covered in this book:

PieceRole
WolfiThe Linux distribution that supplies APK packages
melangeThe tool that builds APK packages from source code. Every one of Wolfi’s packages is built with it
apkoThe tool that assembles an OCI image purely from APK packages

These are developed as separate GitHub repositories, but they function as a single, coherent pipeline through the shared APK package format and shared verification mechanisms — SBOMs and checksums. The next chapter looks back over the whole book and considers where to start if you were to adopt this in your own project.