Kaniko vs Buildah vs Docker-in-Docker: Which Container Build Tool Is Right for You?
Deep comparison of Kaniko (unprivileged, Google archived → community fork), Buildah (rootless, daemonless), and Docker-in-Docker (privileged, Docker socket).
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Production DevOps experience
- ✓Deep understanding of the tool's internals
- ✓Experience debugging distributed systems
Choose Kaniko when you need a simple, unprivileged drop-in replacement for docker build in CI — it runs without root, caches layers in the registry, and works with any Dockerfile. Choose Buildah when you need finer control (rootless builds, multi-architecture without emulation, OCI-compliant images) and are in the Podman/OpenShift ecosystem. Avoid Docker-in-Docker in production CI — the privileged mode requirement is a security risk that doesn't justify the convenience.
Building a Docker image is like cooking from a recipe. DinD is like running a full commercial kitchen inside a food truck (it works, but the kitchen has gas flames and sharp knives that could burn down the truck). Kaniko is like a portable induction cooktop — it can cook the same food, but there's no open flame, and it's much safer. Buildah is like a chef's knife set for a professional kitchen — more control, more tools, but more expertise needed. All three can make the same meal — the question is whether you need the safety of Kaniko or the power of Buildah, and whether you're willing to accept the fire risk of DinD.
For years, Docker-in-Docker (DinD) was the standard approach for building container images inside CI pipelines. You'd start a DinD service container, mount /var/run/docker.sock, and run docker build inside it. It worked — until a CVE in the Docker daemon or a container breakout gave attackers host-level root access. The cloud security industry has since converged on a principle: CI pipelines should never run privileged containers if they can avoid it. Kaniko and Buildah were created specifically to solve this problem.
The security difference is stark. DinD requires --privileged in the container spec, which disables all container isolation features. A simple command injection in your build script can give an attacker root on the host. Kaniko and Buildah run rootless — no privileged mode, no Docker socket, no host kernel access.
But security isn't the only difference. Cache behavior, build performance, ecosystem integration, and debugging capabilities vary significantly. Kaniko's cache stores layers in the registry — efficient for CI but limited compared to BuildKit's cache mounts. Buildah provides finer control but has a steeper learning curve. DinD is easiest to set up but hardest to secure.
Security Comparison: Privileged vs Rootless Builds
The security difference is the single most important factor in choosing a build tool. DinD requires the --privileged flag, which disables all container isolation features — capabilities, seccomp, AppArmor, and device cgroups. The container gets access to the host kernel, all devices, and can load kernel modules. A compromised build process in a privileged container is a host-level root compromise.
Kaniko runs completely unprivileged. It does not require the Docker daemon and can run as a non-root user in the container. It extracts the base image to a snapshot directory, runs each Dockerfile command inside that directory, and snapshots the filesystem after each step. No host kernel access is needed.
Buildah also runs rootless. It uses user namespaces to map the root user inside the build to a non-root user on the host. It does not require any daemon and can build images entirely in userspace. Buildah's rootless mode is natively supported on Podman setups and works on any Linux system with user namespaces enabled.
A security matrix shows the critical differences: DinD requires privileged mode and has full host kernel access. Kaniko runs without any special permissions and has no host kernel access. Buildah rootless runs without privileged mode and maps build root to user namespace.
Kaniko Deep Dive: Cache Management and Configuration
Kaniko builds images by running Dockerfile commands against a snapshot of the base image filesystem. It caches intermediate layers in the container registry using the --cache=true flag. The cache is stored as a separate image in a configurable cache repository (--cache-repo). Each layer is cached with a TTL (--cache-ttl=24h), after which it's re-evaluated.
Kaniko's cache works differently from Docker's layer cache. Docker caches layers on the build host based on instruction text. Kaniko caches layers in the registry based on the SHA of the filesystem snapshot after each instruction. This means Kaniko's cache works across different build hosts (no local cache to share) but is limited by the TTL.
Key flags: --cache=true enables caching, --cache-ttl=24h sets the TTL, --cache-repo=<registry>/<repo> specifies where cache images are stored, --snapshotMode=redo improves cache hit rates for filesystem-heavy builds, --cleanup removes temporary files after the build.
After Google archived the original Kaniko repository, the community fork at github.com/kaniko-project continues active development. The executor image remains at gcr.io/kaniko-project/executor:latest.
--cache=true --cache-ttl=24h reduced average build time from 12 minutes to 3 minutes for their Node.js application. The cache stored the npm ci layer so rebuilds only re-executed the source code COPY and build steps. Without the cache, every build re-downloaded all 200+ npm packages.--cache=true --cache-ttl=24h --snapshotMode=redo for optimal cache performance.Buildah Rootless Builds and Integration with Podman
Buildah provides a CLI for building OCI images that works without a daemon. Its rootless mode uses user namespaces to securely build images without privileged access. The key command is buildah bud (Build Using Dockerfile), which accepts a Dockerfile and produces an OCI image.
Buildah's advantage over Kaniko is finer control over the build process. You can use buildah from to start from a base image, buildah copy to add files, buildah run to execute commands, buildah config to set metadata, and buildah commit to create the final image. This gives you granular control over each layer.
Buildah integrates natively with Podman, which is the container engine for Red Hat OpenShift. If you're in an OpenShift environment, Buildah is the natural choice. It produces OCI-compliant images that work with any container runtime.
For CI/CD, Buildah can run in a container without privileged mode: docker run --security-opt seccomp=unconfined --security-opt apparmor=unconfined quay.io/buildah/stable.
CONFIG_USER_NS=y). Most modern Linux kernels support this, but some CI environments disable it. If user namespaces are unavailable, Buildah can still run but needs --security-opt seccomp=unconfined.Docker-in-Docker: When It Makes Sense and How to Mitigate Risks
Despite its security drawbacks, Docker-in-Docker isn't universally bad — it has legitimate use cases where the alternative tooling isn't available or where the risk is acceptable. Understanding when to use it and how to mitigate its risks is important.
DinD makes sense in: (1) local development environments where you're building inside a Docker container for consistency (not exposed to untrusted code), (2) testing Docker itself or Docker plugins, (3) single-tenant CI runners where you control the build scripts and dependencies, and (4) environments where Kaniko/Buildah compatibility issues prevent migration.
Mitigations for DinD: (1) use --security-opt no-new-privileges:true to prevent privilege escalation, (2) restrict capabilities with --cap-drop=ALL --cap-add=SYS_ADMIN (minimum needed for DinD), (3) use read-only root filesystem, (4) implement strict network policies (egress to known registries only), (5) scan all build dependencies for malicious packages, (6) use short-lived CI runners that are destroyed after each build.
Even with mitigations, DinD should not be used in multi-tenant CI environments (where different users' code runs on the same runner) or in pipelines that handle sensitive credentials.
--privileged or at minimum --cap-add=SYS_ADMIN for the overlay filesystem. Every mitigation is a band-aid — the real fix is migrating to Kaniko or Buildah.Performance Benchmarks: Kaniko vs Buildah vs DinD
Build performance varies significantly across the three tools, depending on the language stack, cache configuration, and build complexity. The following benchmarks are from production CI pipelines and represent typical build times for common application stacks.
Key findings: DinD is generally fastest for first-time builds (no cache) because Docker's layer engine is highly optimized. Kaniko is slightly slower on initial builds (filesystem snapshot overhead) but competitive with caching enabled. Buildah performance is between Kaniko and DinD for most workloads. For cached builds, Kaniko's registry cache is competitive with DinD's local layer cache when properly configured.
The biggest performance difference is in multi-platform builds. DinD requires QEMU emulation (slow). Kaniko has experimental multi-platform support. Buildah supports multi-platform builds natively through its container/image model.
Migration Guide: From DinD to Kaniko or Buildah
Migrating from DinD to Kaniko or Buildah is a common DevOps task. The good news: most Dockerfiles work without modification with Kaniko, and Buildah covers the remaining cases. Here's a step-by-step migration guide.
Step 1: Audit your Dockerfiles. Identify any features that Kaniko/Buildah don't support: ADD with remote URLs, Docker-specific BuildKit features (--mount=type=secret, --mount=type=cache), and docker build flags like --squash. Most standard Dockerfiles work as-is.
Step 2: Choose your tool. Kaniko for simple Dockerfile builds (GitLab CI, AWS CodeBuild, GitHub Actions). Buildah for OpenShift/Podman environments or when you need interactive builds.
Step 3: Update CI configuration. Replace the DinD service container with Kaniko's executor image or Buildah's stable image. Update the build command from docker build to /kaniko/executor or buildah bud.
Step 4: Configure cache. Add --cache=true --cache-ttl=24h for Kaniko. Buildah uses local container storage for caching by default.
Step 5: Test and verify. Build each image, push to registry, and verify the image runs correctly. Compare layer sizes and SBOM contents.
Step 6: Remove privileged flag. Once Kaniko/Buildah is working, remove the privileged: true or --privileged flag from the CI configuration.
DinD Privileged Container Breakout Exposed CI Secrets to an Attacker
--privileged disables all container isolation.docker build inside a DinD service container with privileged: true. A Python dependency in requirements.txt was a typosquatting package that executed a post-install script with root access, escaped to the host via cgroup manipulation, and exfiltrated CI environment variables.- DinD with --privileged is not a 'container' in any security sense — it's a root shell with full host access.
- Any command injection in the build process becomes a host-level compromise when running privileged.
- Kaniko and Buildah were created specifically to eliminate the need for privileged CI runners.
- Dependency typosquatting is a real and growing attack vector — pin with hash verification.
USER root if needed, or set Kaniko's --build-arg for the appropriate user. Use --verbosity=debug to see detailed filesystem snapshots.--userns=keep-id when running the build container. Buildah's --format=docker avoids some OCI permission issues.--cache=true --cache-ttl=24h --cache-repo=<registry>/<cache-repo>. Use --snapshotMode=redo for better cache hit rates on filesystem-heavy builds.buildah bud --format=docker for maximum compatibility.docker build --secret — use --build-arg or mount secrets explicitly. (2) Buildah's --volume syntax differs. (3) Both tools require the full context to be accessible.| File | Command / Code | Purpose |
|---|---|---|
| kaniko-gitlab-ci.yml | build: | Security Comparison |
| kaniko-cache-config.sh | /kaniko/executor \ | Kaniko Deep Dive |
| buildah-rootless-build.sh | buildah bud \ | Buildah Rootless Builds and Integration with Podman |
| dind-gitlab-ci.yml | build: | Docker-in-Docker |
| build-benchmark.sh | docker build -t myapp:test . | Performance Benchmarks |
| github-actions-kaniko.yml | name: Build with Kaniko | Migration Guide |
Key takeaways
docker build with registry-based caching. Most Dockerfiles work without modification.--cache=true --cache-ttl=24h and Buildah's local container storage both reduce rebuild time by 70-90%.Common mistakes to avoid
4 patternsRunning DinD with --privileged in shared CI runners.
Not configuring Kaniko cache and wondering why every build takes 10+ minutes.
Assuming Kaniko supports all Dockerfile features, then failing on ADD with remote URL.
Using Buildah with --format=oci when the deployment environment expects Docker format images.
Interview Questions on This Topic
Compare Kaniko, Buildah, and Docker-in-Docker for building container images in CI. What are the security implications of each?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Docker. Mark it forged?
4 min read · try the examples if you haven't