Kaniko: Building Container Images Inside Kubernetes
Docker-less image builds that don't need a privileged daemon

Contents
There is an awkward chicken-and-egg problem at the heart of running CI inside Kubernetes: you want to build container images, but the traditional way to build a container image is to run docker build, and docker build needs a Docker daemon, and a Docker daemon needs root and a bunch of kernel features that you really, really should not be handing to a CI pod. The old hack — mounting the host’s Docker socket into the build pod — is the security equivalent of leaving your front door open because the lock is fiddly. Anything that can talk to that socket effectively owns the node: it can launch a privileged container, mount the host filesystem, and walk straight out of your cluster’s security boundary. That’s not a theoretical risk; it’s a standard step in real cluster-escape write-ups.
Kaniko, from Google, exists to break that dependency. It builds images from a Dockerfile entirely in userspace, with no Docker daemon and no privileged container, which means it runs as an ordinary pod in an ordinary cluster. You get your image built and pushed to a registry without ever giving CI the keys to the kingdom.
Before we go further: a heads-up that changes the calculus. In June 2025 Google archived the Kaniko repository — it’s read-only, no longer developed by Google, and the old gcr.io/kaniko-project/executor images are frozen. A Chainguard fork (including two of Kaniko’s original authors) picked up maintenance and security patches. Kaniko still works, it’s still a sensible tool, but you need to know which image you’re pulling. I’ll come back to this at the end; the mechanics below are unchanged.
What it does differently
Kaniko ships as a single executor image. You hand it a build context (a directory, a Git repo, or a tarball in object storage), a Dockerfile, and a destination registry. It then reads the Dockerfile, and for each instruction it executes the command and snapshots the resulting filesystem changes in its own userspace, assembling proper image layers as it goes — no daemon, no nested containers.
The catch, and it’s worth understanding, is that Kaniko extracts the base image and runs the build commands directly inside its own container’s filesystem. That’s how it avoids needing privileged mode, but it’s also why you run Kaniko in a disposable pod and never on a machine you care about — it genuinely modifies its own root filesystem during the build. In Kubernetes that’s a non-issue: the pod is ephemeral and gone the moment the build finishes. Run it on your laptop by accident and you’ll be reinstalling.
This design also explains a limitation people trip over: because Kaniko is the build environment, it doesn’t isolate each RUN step in a fresh container the way BuildKit does. For the overwhelming majority of Dockerfiles that’s invisible. For exotic ones, it matters — more on that under gotchas.
A build as a Kubernetes Job
The cleanest way to use Kaniko is a one-shot Job. You need three things: the source somewhere Kaniko can reach, registry credentials mounted as a Docker config.json, and the destination tag. Here’s a build pulling its context straight from a Git repo:
| |
Note what’s absent: no securityContext: privileged: true, no host socket, no special node. The registry-credentials secret is a standard kubernetes.io/dockerconfigjson secret, the same kind you’d use for an image pull secret. Kaniko reads /kaniko/.docker/config.json to authenticate its push. (Point that image: at the Chainguard fork rather than the frozen gcr.io tag for new work — see the verdict.)
Apply it and watch:
| |
That’s a complete build-and-push cycle, no daemon anywhere in sight. If you’re producing minimal runtime images from these builds, Kaniko pairs naturally with a distroless base — the same base image works fine as a Kaniko build target.
Caching, because layers matter
Kaniko’s biggest weakness is build speed — without help it re-runs every layer on every build, because there’s no local daemon cache persisting between ephemeral pods. Each build starts from nothing. The fix is the two flags above. --cache=true makes Kaniko push and pull intermediate layers to a registry-backed cache (--cache-repo), so an unchanged RUN pip install step is fetched rather than re-executed on the next build. It’s not as instant as Docker’s local layer cache, but for CI it turns a five-minute rebuild into a thirty-second one when only your application code changed.
For repeated builds you can also pre-warm base images with the companion kaniko-warmer, which stashes base images in a shared volume so each build doesn’t re-pull python:3.12-slim from scratch. On a homelab cluster with a slow uplink, warming the two or three bases you build against most often is the single biggest speed win available.
Fitting it into a pipeline
In practice you rarely write the Job by hand. CI systems that run on Kubernetes — Tekton, Argo Workflows, GitLab CI with the Kubernetes executor, Drone — all have Kaniko steps or examples, because for years this was the answer to “build images in CI without privilege.” The pattern is always the same: a step that mounts registry credentials and runs the Kaniko executor with --context, --dockerfile, and --destination.
Where the context comes from
The --context flag is more flexible than most people use. Git is convenient for public repos, but for a private homelab pipeline you often don’t want Kaniko cloning anything — you’ve already got the source in the runner. Three patterns cover almost everything:
dir://— the build context is a directory already mounted into the pod, typically via anemptyDirthat a preceding init container populated (agit clone, or an artifact unpacked from a previous CI stage). This is the most common shape inside CI systems.git://— Kaniko clones the repo itself, as shown above. Handy for standalone Jobs; add aKANIKO_GIT_TOKEN-style secret for private repos.s3:///gs:///oci://— the context is a tarball in object storage. Useful when the thing producing the context and the thing building the image are decoupled.
For a private Git host you’ll mount credentials the same way you mount the registry config.json — a secret, referenced by the build. Keep those tokens scoped to read-only on the one repo; a build pod is exactly the kind of ephemeral thing you don’t want holding broad credentials, and the whole reason we reached for Kaniko was to shrink CI’s blast radius.
Reproducibility and the layers you push
One under-appreciated Kaniko flag is --reproducible, which strips timestamps from the image so the same input produces the same digest. That matters more than it sounds: reproducible digests are what make image signing and verification meaningful downstream. If two builds of identical source produce different digests because of embedded build times, you can’t cleanly assert “this digest is the audited one.” The cost is that --reproducible can be slow and memory-hungry on large images, so it’s a trade-off rather than a default — measure it on your own images before turning it on in every pipeline.
Also worth knowing: --single-snapshot collapses the build into one layer (smaller push, loses per-instruction caching), while --cache-copy-layers and --cache-run-layers tune exactly which layer types get cached. The defaults are sensible; reach for these only when a specific build is either too slow or producing bloated images.
Troubleshooting: the sharp edges
A few failures come up often enough to name.
Distroless executor, no shell. The executor image is distroless (no sh), so kubectl exec into a failed build pod gets you nowhere. Use the :debug tag, which bundles a busybox shell, when you need to poke around inside a stuck build.
Unsupported BuildKit features. Kaniko doesn’t implement every modern BuildKit trick. If your Dockerfile leans on RUN --mount=type=cache mounts, secret mounts, or multi-platform buildx magic, check support before committing — you’ll get a parse error or a silently ignored directive rather than a helpful message.
Auth failures on push. “UNAUTHORIZED” at the push stage almost always means the config.json isn’t landing where Kaniko looks. Confirm the secret key is .dockerconfigjson, that it’s mounted at /kaniko/.docker, and that the file is named config.json (the items[].path above does this). A private registry that also needs a CA cert wants that mounted too.
Snapshot memory blowups. Very large build contexts or images with enormous filesystems can push Kaniko’s memory use up during the “Taking snapshot of full filesystem” step. If the pod gets OOM-killed mid-build, raise its memory limit or pass --snapshot-mode=redo/--use-new-run to change how it detects changes.
This is exactly the kind of supply-chain surface worth thinking about end to end — where your base images come from, who can push to your registry, and whether anyone verifies what ran. It’s the same territory as poisoned container images and typosquatting, and the natural next step after building an image is signing it with cosign so consumers can verify it before it runs.
Verdict
If you build images inside Kubernetes and you’ve been mounting the Docker socket to do it, stop and switch to a daemon-less builder — the security upgrade alone is worth the afternoon. Kaniko remains a genuinely good way to produce OCI images from a Dockerfile without a privileged daemon, it slots into every Kubernetes-native CI system, and registry-backed caching keeps it fast enough for real pipelines.
But be honest about its status in 2025. Google archived the original project, so do not build new pipelines against gcr.io/kaniko-project/executor:latest — that image is frozen and won’t get security fixes. Use the actively maintained Chainguard fork instead, or evaluate rootless BuildKit and Buildah, both of which now do daemon-less Kubernetes builds well and are under active development. If you already have Kaniko in production, it’s fine to keep running it while you migrate to the fork; there’s no fire drill. But for a fresh “I have a cluster and I need it to build my images safely” decision today, pick a builder that someone is still maintaining — which now means the Kaniko fork or BuildKit, not the archived original.
The broader lesson outlives the tool. The reason the Docker-socket hack was so tempting, and the reason daemon-less builders exist at all, is that a build pipeline is a soft target: it pulls arbitrary base images, runs arbitrary code from your repo, and holds credentials to push to a registry everyone downstream trusts. Kaniko’s design shrinks the privilege that build step holds; signing the output shrinks what a compromised build can quietly substitute. Whichever builder you land on, keep both halves — least privilege on the build, verification on the result — and the specific tool matters far less than the shape of the pipeline around it.




