Kaniko: building images without Docker, the secure way
Someone told you to "just mount the Docker socket into CI". What you heard was "give every pull request root on the build node". You heard right. Kaniko builds the same Dockerfile with no daemon at all.
The short answer
Build with kaniko: it runs a Dockerfile in userspace inside an ordinary container, with no Docker daemon and no privileged mode. Use the maintained Chainguard fork, build the executor image yourself and pin it by digest, pin base images by digest, build with --reproducible, and give each build one push-only credential.
On this page
What goes wrong
The classic way to build images in CI is to mount the host's Docker socket into the job, or to run Docker-in-Docker in privileged mode. Both hand the job the Docker API of a daemon that runs as root.
Anyone who controls a job can then ask that daemon for anything: a
privileged container, a container with the host's / mounted, or a list of
every other job's containers and secrets on the node. In CI, "anyone who
controls a job" means anyone who can open a pull request that changes the
pipeline or the Dockerfile.
Kaniko removes the daemon. It reads the Dockerfile, pulls the base image, and runs each step inside its own container, then writes the layers to a registry or a tarball. The job has only the rights of its own container.
Two things have changed since most kaniko guides were written. The Google
repository is archived, and the gcr.io/kaniko-project/executor images are
no longer updated. The maintained fork does not publish images, so you build
the executor yourself. That is a supply-chain step in its own right, and
this page does it.
What the docs say
kaniko doesn't depend on a Docker daemon and executes each command within a Dockerfile completely in userspace.
Source: kaniko README
This is a supported replacement of the original
GoogleContainerTools/kanikorepository, which was archived in June of 2025.
Source: chainguard-forks/kaniko README
Binary release artifacts such as container images are not published. The old container images as
gcr.io/kaniko-project/executorandgcr.io/kaniko-project/warmerare unmaintained and no longer updated.
Source: chainguard-forks/kaniko README
First of all, only trusted users should be allowed to control your Docker daemon.
Source: Docker docs, Docker Engine security
The Google README now opens with "This project is archived and no longer
developed or maintained." The fork (the old chainguard-dev/kaniko address
redirects to chainguard-forks/kaniko) also says "No major feature work is
planned." Neither README tells you how to get a trusted executor image now
that none is published; that is left to you.
The secure configuration
1. Build the executor from the fork, once, in a trusted pipeline
The fork vendors its Go dependencies, so the build needs no module downloads after the source tarball.
TAG=v1.25.19
wget https://github.com/chainguard-forks/kaniko/archive/refs/tags/${TAG}.tar.gz
sha256sum ${TAG}.tar.gz # record it; compare it on every rebuild
tar xzf ${TAG}.tar.gz && cd kaniko-${TAG#v}
CGO_ENABLED=0 go build -mod=vendor -trimpath \
-ldflags "-s -w -X github.com/chainguard-dev/kaniko/pkg/version.version=${TAG}" \
-o executor ./cmd/executor2. Package it without a daemon, and pin it
kaniko cannot build its own image from a Dockerfile: it runs from /kaniko,
and copying a new /kaniko/executor over the running one fails. crane
assembles the image instead: a minimal base plus one layer.
mkdir -p layer/kaniko/.docker && cp executor layer/kaniko/executor
tar --owner=0 --group=0 -C layer -cf executor-layer.tar kaniko
STATIC=$(crane digest cgr.dev/chainguard/static:latest)
crane mutate --platform linux/amd64 "cgr.dev/chainguard/static@${STATIC}" \
--append executor-layer.tar \
--entrypoint /kaniko/executor \
-e PATH=/kaniko -e DOCKER_CONFIG=/kaniko/.docker/ -e HOME=/root -e USER=root \
-u 0 -w /workspace \
-t registry.example.com/tools/kaniko-executor:v1.25.19
# crane prints the pushed digest. Pin that digest everywhere, and sign it.3. Build application images with it
# Dockerfile: the base image is pinned by digest, never by a moving tag.
FROM cgr.dev/chainguard/wolfi-base@sha256:<digest>
COPY hello.sh /usr/local/bin/hello.sh
RUN chmod 0555 /usr/local/bin/hello.sh
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/hello.sh"]The build is one ordinary, unprivileged container: no socket, no
--privileged. Written as a docker run (the form the test below used):
docker run --rm \
-v "$PWD:/workspace:ro" \
-v "$PWD/out:/out" \
registry.example.com/tools/kaniko-executor@sha256:<executor-digest> \
--context dir:///workspace \
--dockerfile /workspace/Dockerfile \
--destination registry.example.com/team-a/app:1.0.0 \
--digest-file /out/app.digest \
--reproducible--reproducible strips timestamps so the same inputs give the same digest.
--digest-file records the exact digest that was pushed, which is what you
sign, scan and deploy. For pull requests, add --no-push --tar-path /out/app.tar: the build is tested, and the job holds no push credential at
all.
On Kubernetes, the same arguments go into a Job, and the CI job only
creates that Job and waits for it. The executor image has no shell, so CI
systems that run each step through sh inside the job container cannot use
it directly. Some teams add busybox to a separate "debug" executor for
those systems; the Job keeps the image smaller and the CI job simpler. The companion page on
containing a root kaniko build has the full pod spec: five capabilities,
no service account token, egress to the registry only.
Prove it
The executor built from the fork's source reports its version:
docker run --rm example.com/tools/kaniko-executor:v1.25.19 versionKaniko version : v1.25.19Two builds of the same Dockerfile and context, to a tarball, with no daemon access (log lines about snapshots trimmed):
docker run --rm -v ctx:/workspace:ro -v out:/out example.com/tools/kaniko-executor:v1.25.19 \
--context dir:///workspace --dockerfile /workspace/Dockerfile --destination example.com/team-a/app:1.0 \
--no-push --tar-path /out/app.tar --digest-file /out/app.digest --reproduciblelevel=info msg="Retrieving image cgr.dev/chainguard/wolfi-base@sha256:<digest> from registry cgr.dev"
level=info msg="Building stage 'cgr.dev/chainguard/wolfi-base@sha256:<digest>' [idx: '0', base-idx: '-1']"
level=info msg="COPY hello.sh /usr/local/bin/hello.sh"
level=info msg="RUN chmod 0555 /usr/local/bin/hello.sh"
level=info msg="Running: [/bin/sh -c chmod 0555 /usr/local/bin/hello.sh]"
level=info msg="USER 65532:65532"
level=info msg="ENTRYPOINT [\"/usr/local/bin/hello.sh\"]"
level=info msg="Skipping push to container registry due to --no-push flag"
digest: sha256:357eed1e52a71547c29d70646f4ea6e921eb39dd2fcc063b0fb2124fb8096a71The second build printed the same lines and the same digest:
digest: sha256:357eed1e52a71547c29d70646f4ea6e921eb39dd2fcc063b0fb2124fb8096a71The image runs as a non-root user:
docker load -i out/app.tar && docker run --rm example.com/team-a/app:1.0Loaded image: example.com/team-a/app:1.0
built by kaniko, running as uid 65532For comparison, what a CI job with the Docker socket can do in one line: ask the daemon for a privileged container.
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker:28-cli \
docker run --rm --privileged alpine:3.22 grep CapEff /proc/self/statusCapEff: 000001ffffffffffEvery capability bit set. From there, mounting the host's disk is one more flag.
Mistakes people make
Still pulling gcr.io/kaniko-project/executor:latest
Those images are frozen at v1.24.0 and will not get fixes. A floating tag on an unmaintained image is two problems at once. Build from the fork and pin your own digest.
Trying to build the kaniko image with kaniko
Copying a new executor to /kaniko/executor fails with "text file busy",
because kaniko is running from that path. Assemble the executor image with
crane (or another daemonless tool) instead.
Assuming kaniko makes untrusted builds safe
Kaniko runs as root inside its container. Its README says it "does not make it safe to run untrusted builds". Drop capabilities, remove the service account token, restrict egress, and use a dedicated node pool.
A push credential in every build
A RUN step can read the kaniko Docker config. Pull request builds use
--no-push --tar-path and get no credential. Main-branch builds get a
credential that can push to one repository.
Floating base images
FROM python:3 builds something different every week, and a registry
compromise flows straight into your image. Pin base images by digest and
update them with a reviewed pull request.
Reusing the kaniko container for a second build
Kaniko unpacks the base image over its own filesystem. One build per container, then throw it away.
Checklist
- No CI job mounts the Docker socket or runs privileged Docker-in-Docker.
- The kaniko executor is built from the maintained fork at a pinned tag, with the source tarball hash recorded.
- The executor image is pinned by digest (and signed) in every pipeline.
- Every
FROMin every Dockerfile is pinned by digest. - Builds use
--reproducibleand--digest-file. - Pull request builds use
--no-push --tar-pathand hold no registry credential. - Main-branch builds hold a push credential for one repository only.
- Each build runs in a fresh container or pod.
- The kaniko pod follows the containment settings on the companion page.
The Docker socket was never a build tool; it was a root shell with good marketing. Kaniko builds the same image and hands out a lot less.
H2-CSDE
Learn it on a live range
Building without Docker, in DevSecOps and Supply Chain: a real host in your browser, and every objective checked on the machine.
Start freeH2 Scanner
Want this caught before it merges?
The H2 Scanner runs in your CI and flags the weaknesses pages like this one warn about, on every pull request.
Talk to us