sundaydeploys.dev — bash
  ┌─────────────────────────────────────────┐
  │                                         │
  │    ☽  S U N D A Y   D E P L O Y S  ☽    │
  │                                         │
  │         ~  code · hobbies  ~            │
  │                                         │
  └─────────────────────────────────────────┘
~/sundaydeploys $ cat welcome.txt
I'm a software engineer who spends weekdays writing code and weekends chasing my hobbies

Kaniko vs DinD: Picking the Right Container Build Tool for CI

containers devops gitlab

Note: Google stopped actively maintaining Kaniko in late 2024, and the project is effectively unmaintained. This post captures research I did while the tool was still a going concern. The comparisons are still useful context, but check the current state of the project before adopting it.

Two Ways to Build Images in CI

If you need to build a Docker image inside a CI pipeline, you have two real options. Docker-in-Docker (DinD) runs a full Docker daemon inside your CI container. It is exactly what it sounds like: Docker, inside Docker. You get the real docker build command, all the flags, all the behavior you’re used to locally. The catch is that inner daemon needs --privileged mode to work, which opens a can of worms we’ll get to.

Kaniko takes a different approach. It’s a Go binary that reads your Dockerfile and builds the image entirely in userspace. No daemon, no socket, no elevated privileges. It unpacks the base image into its own filesystem, runs each Dockerfile instruction by manipulating files directly, snapshots the changes into layers, and pushes the result to a registry. It fakes being Docker just well enough to produce a valid image.

The short version: DinD gives you real Docker. Kaniko fakes it.

What Each Tool Actually Needs

DinD’s Moving Parts

DinD in GitLab CI means three things working together. First, the docker:dind service container, which runs the Docker daemon. Second, privileged = true on your GitLab Runner configuration, because the daemon needs kernel-level access that normal containers don’t get. Third, TLS certs for the connection between your job container and the daemon (set via DOCKER_TLS_CERTDIR). After that, it’s just docker build and docker push like you’d run on your laptop.

Kaniko’s Moving Parts

Kaniko needs the executor image (gcr.io/kaniko-project/executor:debug, or more likely a fork image now, but we’ll get to that). The :debug variant is required for GitLab CI because it includes a shell. You also need to write a /kaniko/.docker/config.json file with your registry credentials before the build starts. There’s no docker login command here since Kaniko doesn’t use Docker. Then you call /kaniko/executor with your context, Dockerfile path, and destination registry. No privileged flag needed on the runner.

The Tradeoffs That Actually Matter

Security

DinD’s --privileged flag disables every Linux security mechanism the container runtime provides. AppArmor, seccomp, capability restrictions, all turned off. A compromised build process in a privileged container can escape to the host. This is not theoretical, it’s well-documented, and it’s the whole reason Kaniko exists.

But the security story isn’t as simple as “Kaniko = safe, DinD = dangerous.” Kaniko’s own README says it “by itself does not make it safe to run untrusted builds inside your cluster, or anywhere else.” The build process runs in the same context as your CI job. A malicious RUN instruction in a Dockerfile executes with whatever access Kaniko has. There’s no sandbox.

If you’re building your own team’s Dockerfiles on dedicated runners, the privilege escalation risk with DinD is real but contained. If you’re on shared infrastructure or building untrusted code, neither tool is safe by itself. Sysbox exists as a way to run DinD without --privileged using Linux user namespaces, but it adds operational complexity and isn’t widely deployed.

The honest take: Kaniko has a better default security posture. But “better” is relative, and neither tool is a security boundary for untrusted input.

Caching and Performance

Caching is where most teams actually feel the pain. Both tools have caching, and both have sharp edges.

DinD with ephemeral runners starts from zero every build. The Docker daemon spins up fresh, there’s no layer cache, and every image gets pulled and every layer rebuilt from scratch. You can mitigate this with --cache-from (pull the previous image and use it as a cache source) or BuildKit’s registry caching (--cache-to type=registry), but you have to set it up yourself. Without BUILDKIT_INLINE_CACHE=1, caching silently fails, which is a fun one to debug.

Kaniko’s caching is registry-based by default. You pass --cache=true and --cache-repo, and it checks the registry for each layer before building. This works great across ephemeral runners because the cache lives remotely. But there’s a catch that most comparison posts skip: Kaniko has waterfall cache invalidation. One cache miss means every subsequent layer rebuilds from scratch. If layer 3 of 10 isn’t in the cache, layers 4 through 10 all rebuild even if they haven’t changed. Depending on your Dockerfile, this can wipe out most of the caching benefit.

DinD with BuildKit registry caching has largely closed the gap here. It gets you distributed caching without the waterfall problem.

Performance benchmarks are all over the place. I’ve seen claims that Docker build is 20-40% faster and claims that Kaniko is “significantly more performant.” Neither comes with real numbers behind it. Your mileage will depend on your Dockerfile, your cache hit rate, and your registry latency. Benchmark your own pipeline.

Kaniko’s Archival

Google archived the Kaniko repository in June 2025. The project is officially “no longer developed or maintained.” GitLab removed their Kaniko documentation page.

Two community forks picked it up. Chainguard (whose team includes Kaniko’s original creators) maintains a v1.25.x line with security patches only, no new features. Their free container images are limited to the latest tag. Pinned versions require a paid subscription. The osscontainertools fork is more active, at v1.27.x, with new feature flags, a Dockerfile linter, and free images on GHCR and Docker Hub.

The community is split. Strimzi is formally migrating to Buildah, and Apache Beam has opened a task to replace Kaniko in its SDK builds. Pragmatic teams are swapping gcr.io/kaniko-project/executor:debug for ghcr.io/osscontainertools/kaniko:debug and moving on.

If you choose Kaniko today, you’re choosing a community fork, not a Google-backed project. That’s fine. But you should know it going in.

What This Looks Like in GitLab CI

DinD

build:
  image: docker:27-cli
  services:
    - docker:27-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Requires privileged = true in your runner’s config.toml. That’s the one line that makes security teams nervous.

Kaniko

build:
  image:
    name: ghcr.io/osscontainertools/kaniko:debug
    entrypoint: [""]
  script:
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    - /kaniko/executor
      --context $CI_PROJECT_DIR
      --dockerfile $CI_PROJECT_DIR/Dockerfile
      --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      --cache=true
      --cache-repo=$CI_REGISTRY_IMAGE/cache

No privileged runner needed. The image is from the osscontainertools fork. The auth config is manual and ugly, but it works.

Making the Call

When to Pick Which

Pick DinD if you need full Docker compatibility, you control your runners, and you can accept the privileged requirement. If you already have BuildKit registry caching configured, caching works well. DinD is the path of least surprise for teams that think in docker build.

Pick a Kaniko fork if you can’t run privileged containers. Shared Kubernetes clusters, strict security policies, managed runners you don’t control. If your Dockerfiles are straightforward and you can live with the waterfall cache behavior, Kaniko does the job without the privilege escalation risk.

Starting fresh? If you’re setting up a new pipeline today, BuildKit (via docker buildx) or Buildah are worth evaluating. They’ve absorbed most of Kaniko’s advantages without the maintenance uncertainty. I’m not going to cover them here, but they deserve a look before you commit.

Gotchas Once You’ve Picked

DinD: TLS config is fragile. DOCKER_TLS_CERTDIR misconfiguration means you’ll get cryptic connection errors between the job container and the daemon. Ask me how I know. Nested Docker also gets weird: storage driver stacking, conflicting security profiles, concurrent access to /var/lib/docker. You might not hit these, but when you do, the error messages won’t help.

Kaniko: Auth config is manual. No docker login, you’re writing JSON to a file. Easy to get wrong, annoying to debug. The :debug image is required for GitLab CI because the base image has no shell, which trips up everyone at least once. And not all Dockerfile features work perfectly. Edge cases with multi-stage builds, COPY --from, and build arguments can behave differently than real Docker. Test your specific Dockerfile.