Skip to main content

apko and Wolfi: a Registry-Independent Alternative to Docker Hardened Images

·1100 words·6 mins
Author
Emil Grużalski
Cloud Infrastructure & Automation

The appeal of hardened base images
#

The case for hardened base images is easy to make. A typical node:20 or python:3.12 image ships a full distribution: a shell, a package manager, dozens of libraries you never call, and with them a long tail of CVEs that your scanner will dutifully report every week. Most of those CVEs aren’t exploitable in your app, but they still cost you triage time, they still fail strict gates, and every extra binary is one more thing an attacker can pivot through.

Docker Hardened Images (DHI) are Docker’s answer to this: minimal, near-zero-CVE base and runtime images, signed, with SBOMs and provenance attached, often in distroless and FIPS variants. They’re genuinely good. If you’re already all-in on Docker’s ecosystem, they’re an easy win.

Where they didn’t fit for me
#

My blocker was operational, not technical. The organization standardizes on GitHub Container Registry (GHCR) — that’s where images are built, scanned, signed, and pulled from across CI and production. Adopting DHI would have meant pulling base images from Docker’s own registry, behind a Docker Hardened Images subscription, with the corresponding registry login and entitlement wired into every build and every node.

That’s a second registry dependency, a second set of credentials, and a commercial entitlement to manage — all to get base images we then rebuild and push into GHCR anyway. For a shop that has deliberately consolidated on GHCR, taking on dhi.io as a hard dependency in the critical path of every build wasn’t something I wanted to sign up for, at least not yet.

So I went looking for something that gives the same security properties but lets me own the artifact and host it wherever I want. That’s apko and Wolfi.

What Wolfi is
#

Wolfi is a community Linux undistro from the people at Chainguard. Calling it an “undistro” is deliberate: it has no installer, no kernel, no init system. It exists purely to be the userspace inside a container.

A few things make it well suited to hardened images:

  • glibc-based, not musl — so unlike Alpine you don’t hit the subtle compatibility issues with prebuilt binaries, native Node modules, or Python wheels.
  • Granular packages built specifically for containers, so you can include exactly what you need and nothing else.
  • Rolling, fast-moving CVE patching — packages are rebuilt and pushed quickly, so the “fix the CVE” lag is short.
  • Signed packages and SBOMs by default, which is the whole point.

Crucially, the Wolfi package repository (packages.wolfi.dev) is public and free to pull from. You don’t need an account or a subscription to consume the packages — that’s exactly what made it a fit for my constraint.

What apko is
#

apko is the build tool. Instead of a Dockerfile with imperative RUN steps, you describe the image declaratively in YAML: which repositories to pull packages from, which packages to install, which user to run as, and what the entrypoint is. apko then assembles an OCI image directly from those apk packages — no intermediate docker build, no layers full of build cruft.

What you get out of it:

  • Reproducible, often bit-for-bit identical builds — the same config produces the same image, which is a real supply-chain property, not a slogan.
  • An SBOM generated automatically for every image it builds.
  • Multi-arch images (x86_64, aarch64) from one config.
  • Tiny images — because you only add the packages you list, there’s no shell or package manager unless you explicitly ask for one. That’s distroless by construction.
  • It can push directly to any OCI registry — including GHCR.

apko is frequently paired with melange, a sibling tool that builds apk packages from source (also reproducibly, with SBOMs). For most app images you don’t need melange — you consume existing Wolfi packages with apko and add your own application binary.

What it looks like
#

A minimal apko config for a static Go binary is about this small:

contents:
  repositories:
    - https://packages.wolfi.dev/os
  keyring:
    - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
  packages:
    - ca-certificates-bundle

accounts:
  groups:
    - groupname: nonroot
      gid: 65532
  users:
    - username: nonroot
      uid: 65532
      gid: 65532
  run-as: 65532

entrypoint:
  command: /usr/bin/myapp

archs:
  - x86_64
  - aarch64

No base image line, because there is no base image in the Dockerfile sense — apko is assembling the image from packages. There’s no shell and no package manager in the result, just ca-certificates, your binary, and a non-root user.

Building and pushing straight to GHCR is a single command:

apko publish apko.yaml ghcr.io/myorg/myapp:1.0.0

That builds the multi-arch image, generates the SBOM, and pushes the result to my registry. There’s no second registry to authenticate against, no subscription entitlement in the critical path — the only external dependency is the public Wolfi package repo, and the artifact lands in GHCR exactly like every other image in the org.

In CI it slots in cleanly: there’s an official GitHub Action for apko, and you sign the pushed image with cosign and the workflow’s OIDC identity, the same way you’d sign anything else in GHCR.

So what does this actually buy you
#

Lined up against a stock node/python/debian base, the apko + Wolfi image gives you:

  • A dramatically smaller attack surface — no shell, no package manager, no libraries you didn’t ask for.
  • Far fewer CVEs to triage, and faster upstream fixes when one does appear.
  • An SBOM and reproducible build out of the box, which makes provenance and policy checks straightforward.
  • A declarative, reviewable image definition — the YAML diff tells you exactly what changed in the image, unlike a pile of RUN lines.

And against DHI specifically, the trade is clear: DHI hands you a curated, supported, ready-made image but ties you to Docker’s registry and subscription. apko + Wolfi asks you to do the (small) assembly yourself, and in return the image is yours — built in your CI, hosted in your registry, with no external entitlement in the pull path. For an organization that has standardized on GHCR, that registry independence was the deciding factor.

When DHI is still the better call
#

This isn’t an argument that DHI is wrong. If you want a vendor-supported image with an SLA, patched and certified for you, and the Docker subscription and registry are already part of your world, DHI is the lower-effort path and a perfectly good one. The point is only that hardened images are not a thing you can only buy — with apko and Wolfi you can build them yourself, and host them wherever your organization already lives. For me, that was GHCR, and that made all the difference.

Both tools are open source: apko and Wolfi.