# Assembling a Container Image with apko

> Source: https://www.ymotongpoo.com/books/chainguard-image-toolchain/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:

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

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

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

```shell
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](https://github.com/google/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](https://github.com/chainguard-dev/apko) 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.

