Self-hosted service
chainguard-dev/apko avatar
chainguard-dev/apko

apko drops RUN from the build and gets reproducible images because of it

Build OCI images from APK packages directly without Dockerfile

1,696 stars233 forksGoApache-2.0

At a glance

What is it?
apko builds OCI images straight from apk packages with no Dockerfile and no RUN statements, which is what lets it claim bitwise reproducible output and a complete SBOM. The cost is that anything bespoke must already exist as an apk package, so the real adoption question is where your custom binaries come from.
Who is it for?
Adopt apko if your images are assembled from existing apk packages and you need bitwise reproducible output plus an SBOM that covers the whole filesystem, which is exactly the case for CI rebuilding images on every upstream patch. Do not adopt it if your build needs to compile, generate or download anything during the image build, because apko has no RUN equivalent and refuses to pretend otherwise.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The missing RUN statement is the whole design

apko builds OCI container images from apk packages without a Dockerfile. The absence of arbitrary commands during the build is deliberate, and the README states it plainly: by design, apko does not support an equivalent of RUN statements in Dockerfiles.

That single decision explains the properties the project advertises. If the only inputs are a package list and a repository URL, then the contents of the image are fully determined, and apko can make stronger statements about what is inside than a Dockerfile-based build can. Two follow from directly: the images are fully bitwise reproducible, and SBOMs can cover the complete contents. Reproducibility is stated as a default guarantee rather than a flag. Run apko twice and you get exactly the same binary.

The other advertised properties come from the same source. Images are small because they contain only what the application needs, in the distroless style. Builds are fast because there is no command execution, with the goal stated as building images in ms, though the README gives no measurement behind that figure. An SBOM is produced detailing all the packages inside. And for containers that need several processes, apko supports the s6 supervision suite to run them without reaping or signalling issues, which is the problem you hit when a shell-form entrypoint and an init-less container disagree about who reaps orphans.

The design was heavily influenced by ko and distroless, and apko was created by Chainguard, which needs secure, reproducible images for its tooling and has to rebuild constantly as new versions and patches land.

A YAML file with a repository, a package list and an entrypoint

The image definition is a YAML file. The README's quickstart uses the Alpine base example, which is small enough to read in full.

yaml
contents:
  repositories:
    - https://dl-cdn.alpinelinux.org/alpine/v3.22/main
    packages:
      - alpine-base

entrypoint:
  command: /bin/sh -l

# optional environment configuration
environment:
  PATH: /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin

Four things are happening. contents.repositories lists the APK repositories to resolve packages from, here one URL pinning Alpine 3.22 main. contents.packages is the list of package names, here a single metapackage called alpine-base. entrypoint.command is given as a single string, /bin/sh -l, which matters because apk packages do not carry entrypoint metadata the way a Dockerfile's ENTRYPOINT does. environment is explicitly marked optional, and PATH is spelled out in full rather than inherited.

The apko file format supports considerably more than this. The examples directory in the repository names the shape of the surface: per-architecture files such as alpine-amd64.yaml, a rootless variant, python3 and go images, nginx in both root and rootless form, wolfi-base.yaml and wolfi-go.yaml for the Wolfi distribution, certificates and package-certificates, a runtime-keyring example, an alias-only file, an include file, an overlay file, an options file, a dev environment file, an on_top_of_base directory, an old-glibc example, and abseil-regression.yaml. Those filenames alone tell you the format handles architecture pinning, keyring and certificate management, overlays, includes, aliases and building on top of an existing base. The format itself is documented at docs/apko_file.md.

The command surface has two verbs that matter. apko build writes a tarball you then load or push by hand. apko publish talks to a registry directly.

Four install routes and a container-first workflow

apko is a Go program distributed four ways, and the README lists all four.

shell
brew install apko
shell
mise use -g apko@latest

The mise invocation is the one to note for reproducibility work, since it takes an explicit version, and the README notes the version is specified there with latest used as the sample. Installing from source is a plain go install against the module path:

shell
go install chainguard.dev/apko@latest

And for machines where you would rather not install a binary at all, the project publishes its own image:

shell
docker run cgr.dev/chainguard/apko version

Because the container has no access to your filesystem by default, the README explains that to use the examples you generally want to mount your current directory into the container, which is why the build command in the containerized flow carries a volume flag and a /work path:

shell
docker run -v "$PWD":/work cgr.dev/chainguard/apko build examples/alpine-base.yaml apko-alpine:edge apko-alpine.tar

Note the shape of that command, because it is the same everywhere: a config file, an image name, and an output path. Once the tar exists you load it into Docker with docker load, and Docker reports the loaded image name. To run it you append your processor architecture to the tag, which the README writes as the image tag followed by a hyphen and the architecture. That detail is a documentation placeholder rather than a literal string, so expect to substitute your own architecture.

Build, load, run, or publish straight to a registry

The local path takes three commands once you have the binary and a config file.

shell
apko build examples/alpine-base.yaml apko-alpine:test apko-alpine.tar

The output is worth reading, because the log lines tell you what apko actually produces. It generates an SBOM, it reports that it is building an OCI image from a layer tarball in a temporary directory, it prints both the OCI layer digest and the layer diffID as sha256 values, and it confirms the output image file path. Two digests rather than one is normal OCI behaviour, since a diffID covers the uncompressed layer content and the digest covers the compressed layer, and having both in the log makes the build verifiable afterwards.

Loading the result into Docker is a shell redirection, and the confirmation Docker prints is the image name you chose:

shell
docker load < apko-alpine.tar

Publishing is a single verb with no intermediate file:

shell
apko publish examples/alpine-base.yaml myrepo/alpine-apko:test

The README states that this can be done from any environment with apk tooling, which is a stronger requirement than it sounds. Because the build resolves packages from remote repositories and unpacks their contents, you need apk tooling available locally to do that work, which is why the alpine-base example works on macOS and on Alpine itself but the same config would need adjustment for a distribution whose package manager is not apk-based. Cross-building for another architecture is what the per-architecture example files are for, since the example name alpine-amd64.yaml exists precisely because the package set differs by target.

Where apko stops and melange starts

The limitation is stated in the declarative section and it is not a bug. To install bespoke tooling or applications into an image, they must first be packaged into an apk. There is no way around this inside apko, because allowing arbitrary commands would undo the reproducibility guarantee.

The workaround is the sister project, melange, which is designed to produce the apk packages apko consumes. The README's position is that melange and apko together cover the vast majority of container image use cases, and where they are not a good fit the recommendation is a hybrid: build a base image with apko and melange, then use traditional tooling such as Dockerfiles for the final step.

That hybrid advice is the honest answer for most teams with a compiled application. Building a Go binary needs the toolchain, and the toolchain has to come from somewhere. The realistic sequence is melange packages the binary, apko assembles the runtime image around it, and you accept that the actual compile happened outside the image build.

The examples directory shows what that looks like in practice. examples/wolfi-go.yaml and examples/alpine-python3.yaml are language images, and examples/on_top_of_base/ is a directory, which suggests the layering capability has its own set of examples separate from single-file definitions.

There is a README notice that apko is a work in progress and details are subject to change, and the repository carries a NEWS.md alongside release.md and a CHANGELOG-style history. Given release numbering, the project ships frequently: v1.4.9, v1.4.10 and v1.4.11 all landed within the first half of October 2026, and the last push was on 2026-10-15.

The build itself is reproducible down to the build date

The Makefile is where the reproducibility claim gets its teeth, and it is more careful than most. Version metadata is assembled from git rather than from the wall clock, using git describe with tags, the commit hash from git rev-parse, and a tree state that flips to dirty when git diff reports changes.

makefile
GIT_VERSION ?= $(shell git describe --tags --always --dirty)
GIT_HASH ?= $(shell git rev-parse HEAD)
SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct)
BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)")

The SOURCE_DATE_EPOCH handling is the standard trick for reproducible builds, and here it falls back across GNU date, BSD date and finally plain UTC so the same Makefile works on Linux and macOS. Note that the git-derived version and the derived build date feed LDFLAGS through the release-utils version package, which is how a Go binary ends up with an embedded build timestamp that still matches between two machines.

The build is also done with ko, targeting multiple platforms at once. The ko target sets a bare build, passes --platform=all, and applies three tags at once: the image tag, the git version and the git hash. Three tags on one image means you can pull by any of them, which is what makes it practical to reference a build in a deployment manifest and still be able to trace it back to a commit.

The Go module itself requires go 1.27.0 and includes go-containerregistry, cobra, compression libraries, packageurl-go, Prometheus client, OpenTelemetry and step crypto among its dependencies.

Editorial conclusion

Adopt apko if your images are assembled from existing apk packages and you need bitwise reproducible output plus an SBOM that covers the whole filesystem, which is exactly the case for CI rebuilding images on every upstream patch. Do not adopt it if your build needs to compile, generate or download anything during the image build, because apko has no RUN equivalent and refuses to pretend otherwise. Verify first that your custom tooling is packaged as an apk, which in practice means using melange or accepting that you must build a base with apko and finish the job with a Dockerfile.

Frequently asked questions

How do I install apko?

Four routes are given: brew install apko, mise use -g apko@latest which lets you pin a version, go install chainguard.dev/apko@latest for a source install, or docker run cgr.dev/chainguard/apko version to use the published container image without installing anything.

Can apko run commands while building an image like a Dockerfile RUN step?

No, and that is by design. The README states apko does not support an equivalent of RUN statements, which is what makes its files fully declarative and its images bitwise reproducible. Bespoke tooling has to be packaged as an apk first, using the melange project.

How do I publish an image built by apko to a registry?

Use apko publish with a config file and an image reference, for example apko publish examples/alpine-base.yaml myrepo/alpine-apko:test. Building to a tarball first with apko build and loading it with docker load is the local alternative.

Official sources

  1. chainguard-dev/apko on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/chainguard-dev-apko.svg)](https://hysenlabs.com/projects/chainguard-dev-apko)

Was this useful?