Google archived the original Kaniko project in June 2025, but the tool lives on through the Chainguard fork maintained by the original creators. Kaniko remains the go-to solution for building container images inside Kubernetes without requiring Docker daemon access or privileged containers.
This guide walks through building and pushing container images from a Dockerfile using Kaniko in Kubernetes. We cover both simple and multi-stage builds, pushing to Docker Hub, GHCR, Amazon ECR, and other registries, with every command tested on a live cluster.
Tested April 2026 | Kubernetes v1.34.6 (K3s), Ubuntu 24.04 LTS, Kaniko (Chainguard fork via GitLab registry)
What happened to Kaniko?
Google archived the original Kaniko repository in June 2025. The last release from Google was v1.24.0. The old container images at gcr.io/kaniko-project/executor are unmaintained and will not receive security patches.
The project forked in two directions:
- Chainguard fork (chainguard-forks/kaniko): Maintained by the original Kaniko creators (Priya Wadhwa, Dan Lorenc). Focused on security patches and dependency updates. Current version: v1.25.12.
- osscontainertools fork (osscontainertools/kaniko): A more aggressive fork adding new features, currently at v1.27.2.
GitLab publishes free, ready-to-use container images built from the Chainguard fork at registry.gitlab.com/gitlab-ci-utils/container-images/kaniko. This is what we use throughout this guide because it is free, multi-platform (amd64/arm64), and actively maintained.
How Kaniko builds images
Unlike docker build, Kaniko does not need a Docker daemon. It runs entirely in userspace inside a container:
- Reads the Dockerfile and fetches the base image from the registry
- Extracts the base image filesystem inside the container
- Executes each Dockerfile instruction (RUN, COPY, ADD) in order
- Takes a filesystem snapshot after each command to create image layers
- Pushes the final image (with all layers) to the destination registry
Because it never touches the host’s Docker socket or container runtime, Kaniko needs no privileged access. This makes it safe for multi-tenant Kubernetes clusters where giving pods access to the Docker socket would be a security risk.
Which Kaniko image to use
The old gcr.io/kaniko-project/executor image is dead. Here are the current options:
| Image | Source | Cost | Notes |
|---|---|---|---|
registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:debug | GitLab (Chainguard fork) | Free | Recommended. Tracks latest, includes shell |
registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:vX.Y.Z-debug | GitLab (Chainguard fork) | Free | Pinned version for reproducible builds (e.g., v1.25.12-debug) |
cgr.dev/chainguard/kaniko | Chainguard Images | Paid | Hardened, FIPS variants available |
The debug variant includes a busybox shell, which is essential when running Kaniko in CI/CD pipelines or when you need to troubleshoot build failures. The non-debug variant has no shell at all.
Prerequisites
- A running Kubernetes cluster (any distribution: kubeadm, K3s, EKS, GKE, AKS)
kubectlconfigured with cluster access- A container registry account (Docker Hub, GHCR, ECR, or any OCI-compliant registry)
- Tested on: Kubernetes v1.34.6, Ubuntu 24.04.4 LTS, Kaniko (Chainguard fork,
:debugtag)
Create a registry credential secret
Kaniko needs credentials to push built images to your container registry. Create a Kubernetes secret that Kaniko pods will mount.
For Docker Hub:
kubectl create secret docker-registry docker-hub-secret \
--docker-server=https://index.docker.io/v1/ \
--docker-username=your-dockerhub-username \
--docker-password=your-dockerhub-token \
[email protected]
For GitHub Container Registry (GHCR):
kubectl create secret docker-registry ghcr-secret \
--docker-server=ghcr.io \
--docker-username=your-github-username \
--docker-password=your-github-pat
For Amazon ECR and Google Artifact Registry, see the dedicated sections below.
Verify the secret was created:
kubectl get secret docker-hub-secret -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | python3 -m json.tool
You should see the registry URL and your encoded credentials in the output.
Build a simple container image
Start with a straightforward Nginx image to confirm Kaniko is working. Create a Dockerfile and an HTML file, then pass them to Kaniko as a ConfigMap.
Create the build context files:
mkdir -p /tmp/kaniko-demo
cat > /tmp/kaniko-demo/Dockerfile << 'DEOF'
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
DEOF
cat > /tmp/kaniko-demo/index.html << 'HEOF'
Built with Kaniko
Container built with Kaniko in Kubernetes
This image was built without Docker daemon access.
HEOF
Create a ConfigMap from these files so the Kaniko pod can access them:
kubectl create configmap kaniko-build-context \
--from-file=/tmp/kaniko-demo/Dockerfile \
--from-file=/tmp/kaniko-demo/index.html
The ConfigMap shows up in the cluster:
configmap/kaniko-build-context created
Now create the Kaniko pod manifest. Replace your-dockerhub-username with your Docker Hub username:
cat > kaniko-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: kaniko-build
spec:
containers:
- name: kaniko
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:debug
args:
- --context=dir:///workspace
- --dockerfile=/workspace/Dockerfile
- --destination=docker.io/your-dockerhub-username/kaniko-demo:latest
- --verbosity=info
volumeMounts:
- name: build-context
mountPath: /workspace
- name: docker-config
mountPath: /kaniko/.docker/
restartPolicy: Never
volumes:
- name: build-context
configMap:
name: kaniko-build-context
- name: docker-config
secret:
secretName: docker-hub-secret
items:
- key: .dockerconfigjson
path: config.json
EOF
A few things to note about this manifest:
--context=dir:///workspacetells Kaniko where to find the Dockerfile and build context--destinationsets the registry and image name to push to- The Docker Hub secret is mounted at
/kaniko/.docker/where Kaniko expects registry credentials restartPolicy: Neverbecause this is a one-shot build, not a long-running service
Apply the manifest:
kubectl apply -f kaniko-pod.yaml
Watch the build progress in real time:
kubectl logs -f kaniko-build
Kaniko pulls the base image, executes each Dockerfile instruction, and pushes the result:
INFO[0000] Retrieving image manifest nginx:alpine
INFO[0000] Retrieving image nginx:alpine from registry index.docker.io
INFO[0003] Built cross stage deps: map[]
INFO[0003] Executing 0 build triggers
INFO[0003] Building stage 'nginx:alpine' [idx: '0', base-idx: '-1']
INFO[0003] Unpacking rootfs as cmd COPY index.html /usr/share/nginx/html/index.html requires it.
INFO[0011] COPY index.html /usr/share/nginx/html/index.html
INFO[0011] Taking snapshot of files...
INFO[0011] EXPOSE 80
INFO[0011] Cmd: EXPOSE
INFO[0011] Adding exposed port: 80/tcp
INFO[0011] CMD ["nginx", "-g", "daemon off;"]
INFO[0011] Pushing image to docker.io/your-dockerhub-username/kaniko-demo:latest
The pod status changes to Completed when the build and push finish:
kubectl get pod kaniko-build
The output confirms the build completed successfully:
NAME READY STATUS RESTARTS AGE
kaniko-build 0/1 Completed 0 37s
Multi-stage builds
Kaniko handles multi-stage Dockerfiles the same way Docker does. This is where it gets practical: compile your application in one stage, copy the binary into a minimal runtime image in the next.
Here is a Go application built as a multi-stage image:
cat > /tmp/kaniko-demo/Dockerfile << 'DEOF'
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go mod init kaniko-demo && CGO_ENABLED=0 go build -o /app/server .
FROM alpine:3.21
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
DEOF
cat > /tmp/kaniko-demo/main.go << 'GOEOF'
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Kaniko-built container!")
})
http.ListenAndServe(":8080", nil)
}
GOEOF
Recreate the ConfigMap with the updated files:
kubectl delete configmap kaniko-build-context
kubectl create configmap kaniko-build-context \
--from-file=/tmp/kaniko-demo/Dockerfile \
--from-file=/tmp/kaniko-demo/main.go
This time, use a Kubernetes Job instead of a bare Pod. Jobs handle retries and completion tracking, which is better for CI/CD workflows:
cat > kaniko-job.yaml << 'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-multistage-build
spec:
backoffLimit: 0
template:
spec:
containers:
- name: kaniko
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:debug
args:
- --context=dir:///workspace
- --dockerfile=/workspace/Dockerfile
- --destination=docker.io/your-dockerhub-username/kaniko-go-demo:v1.0
- --destination=docker.io/your-dockerhub-username/kaniko-go-demo:latest
- --cache=true
- --verbosity=info
volumeMounts:
- name: build-context
mountPath: /workspace
- name: docker-config
mountPath: /kaniko/.docker/
restartPolicy: Never
volumes:
- name: build-context
configMap:
name: kaniko-build-context
- name: docker-config
secret:
secretName: docker-hub-secret
items:
- key: .dockerconfigjson
path: config.json
EOF
The --cache=true flag tells Kaniko to push intermediate layer caches to the registry. On subsequent builds, Kaniko checks for cached layers before re-executing commands, which speeds up rebuilds significantly. The --destination flag appears twice because we are tagging the image with both v1.0 and latest.
Run the job:
kubectl apply -f kaniko-job.yaml
Monitor the build:
kubectl logs -f job/kaniko-multistage-build
The output shows Kaniko processing both build stages, compiling the Go binary, then creating the final minimal image:
INFO[0001] Resolved base name golang:1.24-alpine to builder
INFO[0001] Retrieving image manifest golang:1.24-alpine
INFO[0001] Retrieving image golang:1.24-alpine from registry index.docker.io
INFO[0007] Built cross stage deps: map[0:[/app/server]]
INFO[0007] Building stage 'golang:1.24-alpine' [idx: '0', base-idx: '-1']
INFO[0025] WORKDIR /app
INFO[0025] Cmd: workdir
INFO[0025] Changed working directory to /app
INFO[0025] RUN go mod init kaniko-demo && CGO_ENABLED=0 go build -o /app/server .
INFO[0028] Running: [/bin/sh -c go mod init kaniko-demo && CGO_ENABLED=0 go build -o /app/server .]
INFO[0028] Pushing layer to cache now
INFO[0083] Saving file app/server for later use
INFO[0083] Deleting filesystem...
INFO[0084] Building stage 'alpine:3.21' [idx: '1', base-idx: '-1']
INFO[0086] COPY --from=builder /app/server /server
INFO[0086] Taking snapshot of files...
INFO[0086] EXPOSE 8080
INFO[0086] ENTRYPOINT ["/server"]
INFO[0086] Pushing image to docker.io/your-dockerhub-username/kaniko-go-demo:v1.0
INFO[0100] Pushed image to docker.io/your-dockerhub-username/kaniko-go-demo:v1.0
INFO[0100] Pushing image to docker.io/your-dockerhub-username/kaniko-go-demo:latest
INFO[0102] Pushed image to docker.io/your-dockerhub-username/kaniko-go-demo:latest
The job completes in under 2 minutes:
kubectl get jobs
Both tags pushed successfully, and the job shows as complete:
NAME STATUS COMPLETIONS DURATION AGE
kaniko-multistage-build Complete 1/1 106s 2m14s
Verify the built image
Deploy the Kaniko-built image to your cluster to confirm it works:
kubectl run kaniko-test-app --image=docker.io/your-dockerhub-username/kaniko-go-demo:latest --port=8080
kubectl expose pod kaniko-test-app --type=NodePort --port=8080
Check that the pod starts successfully:
kubectl get pod kaniko-test-app -o wide
The pod should be running within seconds:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kaniko-test-app 1/1 Running 0 12s 10.42.0.14 k8s-master <none> <none>
Curl the pod IP to verify the Go application responds:
POD_IP=$(kubectl get pod kaniko-test-app -o jsonpath='{.status.podIP}')
curl http://$POD_IP:8080
The application responds as expected:
Hello from Kaniko-built container!
Push to Amazon ECR
Kaniko includes a built-in ECR credential helper. Create the ECR repository and Kubernetes secret, then point Kaniko at it.
Create the ECR repository (if it does not already exist):
aws ecr create-repository --repository-name kaniko-demo --region us-east-1
Generate an ECR login token and create the Kubernetes secret:
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AWS_REGION=us-east-1
ECR_TOKEN=$(aws ecr get-login-password --region $AWS_REGION)
kubectl create secret docker-registry ecr-secret \
--docker-server=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com \
--docker-username=AWS \
--docker-password=$ECR_TOKEN
ECR tokens expire after 12 hours. For production use, look into IAM Roles for Service Accounts (IRSA) or a CronJob that refreshes the secret.
The Kaniko Job manifest for ECR looks like this:
cat > kaniko-ecr.yaml << 'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-ecr-push
spec:
backoffLimit: 0
template:
spec:
containers:
- name: kaniko
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:debug
args:
- --context=dir:///workspace
- --dockerfile=/workspace/Dockerfile
- --destination=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/kaniko-demo:latest
- --destination=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/kaniko-demo:v1.0
- --verbosity=info
volumeMounts:
- name: build-context
mountPath: /workspace
- name: docker-config
mountPath: /kaniko/.docker/
restartPolicy: Never
volumes:
- name: build-context
configMap:
name: kaniko-build-context
- name: docker-config
secret:
secretName: ecr-secret
items:
- key: .dockerconfigjson
path: config.json
EOF
After the job completes, verify the image landed in ECR:
aws ecr describe-images --repository-name kaniko-demo --region us-east-1 \
--query 'imageDetails[*].{Tags:imageTags,PushedAt:imagePushedAt,Size:imageSizeInBytes}' \
--output table
The output confirms both tags are present in the repository:
-------------------------------
| DescribeImages |
+----------------+------------+
| PushedAt | Size |
+----------------+------------+
| 1775771832.65 | 26001123 |
+----------------+------------+
|| Tags ||
|+---------------------------+|
|| v1.0 ||
|| latest ||
|+---------------------------+|
Push to Google Artifact Registry
Google Artifact Registry uses OAuth2 access tokens for authentication. Create the repository and secret, then build with Kaniko.
Create the Artifact Registry Docker repository:
gcloud artifacts repositories create kaniko-demo \
--repository-format=docker \
--location=europe-west1 \
--description="Kaniko builds"
Generate an access token and create the Kubernetes secret:
GCP_REGION=europe-west1
GCP_PROJECT=$(gcloud config get-value project)
GCP_TOKEN=$(gcloud auth print-access-token)
kubectl create secret docker-registry gar-secret \
--docker-server=${GCP_REGION}-docker.pkg.dev \
--docker-username=oauth2accesstoken \
--docker-password=$GCP_TOKEN
GCP access tokens also expire (after 1 hour by default). For GKE clusters, use Workload Identity instead of static tokens. For non-GKE clusters, use a service account key or refresh the secret via a CronJob.
Set the destination to your Artifact Registry path (REGION-docker.pkg.dev/PROJECT/REPO/IMAGE:TAG):
--destination=europe-west1-docker.pkg.dev/your-project/kaniko-demo/your-app:latest
After pushing, list the images with gcloud:
gcloud artifacts docker images list \
europe-west1-docker.pkg.dev/your-project/kaniko-demo \
--include-tags \
--format="table(package,tags,createTime)"
The output shows the image with both tags and the creation timestamp:
IMAGE TAGS CREATE_TIME
europe-west1-docker.pkg.dev/your-project/kaniko-demo/nginx-app latest,v1.0 2026-04-10T00:57:08
Build from a Git repository
For most CI/CD pipelines, the build context lives in a Git repository rather than a ConfigMap. Kaniko can clone a repository directly.
Public repository:
cat > kaniko-git-build.yaml << 'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-git-build
spec:
backoffLimit: 0
template:
spec:
containers:
- name: kaniko
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:debug
args:
- --context=git://github.com/your-org/your-repo.git#refs/heads/main
- --dockerfile=Dockerfile
- --destination=docker.io/your-dockerhub-username/your-app:latest
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
restartPolicy: Never
volumes:
- name: docker-config
secret:
secretName: docker-hub-secret
items:
- key: .dockerconfigjson
path: config.json
EOF
Private repository: set the GIT_TOKEN environment variable. Kaniko uses it for HTTPS authentication:
cat > kaniko-private-git.yaml << 'EOF'
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-private-build
spec:
backoffLimit: 0
template:
spec:
containers:
- name: kaniko
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:debug
env:
- name: GIT_TOKEN
valueFrom:
secretKeyRef:
name: git-credentials
key: token
args:
- --context=git://github.com/your-org/private-repo.git#refs/heads/main
- --dockerfile=Dockerfile
- --destination=docker.io/your-dockerhub-username/your-app:latest
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
restartPolicy: Never
volumes:
- name: docker-config
secret:
secretName: docker-hub-secret
items:
- key: .dockerconfigjson
path: config.json
EOF
Create the Git token secret first:
kubectl create secret generic git-credentials --from-literal=token=ghp_your_github_pat_here
The --context-sub-path flag is useful when the Dockerfile is not in the repository root:
--context=git://github.com/your-org/monorepo.git#refs/heads/main
--context-sub-path=services/api
--dockerfile=Dockerfile
Kaniko build arguments reference
These are the most commonly used Kaniko flags:
| Flag | Description | Example |
|---|---|---|
--context | Build context location | dir:///workspace, git://github.com/org/repo.git, s3://bucket/path |
--dockerfile | Path to Dockerfile within context | /workspace/Dockerfile |
--destination | Registry and image name (repeatable for multi-tag) | docker.io/user/app:v1.0 |
--cache | Enable layer caching in the registry | --cache=true |
--cache-repo | Custom cache repository | docker.io/user/app/cache |
--no-push | Build only, do not push (good for testing) | --no-push |
--build-arg | Pass build arguments to Dockerfile | --build-arg=VERSION=1.0 |
--target | Build up to a specific stage | --target=builder |
--context-sub-path | Subdirectory within the context | --context-sub-path=services/api |
--verbosity | Log level | info, debug, warn, error |
--snapshot-mode | Controls snapshot behavior | full (default), redo, time |
--insecure | Push to HTTP registries | --insecure |
Supported build context sources
Kaniko supports several build context locations beyond local directories and Git repositories:
| Source | Context format | Auth method |
|---|---|---|
| Local directory | dir:///path | Volume mount |
| Git repository | git://github.com/org/repo.git#ref | GIT_TOKEN env var |
| AWS S3 | s3://bucket/path/context.tar.gz | AWS credentials / IRSA |
| Google Cloud Storage | gs://bucket/path/context.tar.gz | GCP service account |
| Azure Blob Storage | https://account.blob.core.windows.net/container/context.tar.gz | Azure credentials |
For S3 and GCS contexts, tar the build context and upload it to your bucket:
tar -czf context.tar.gz Dockerfile main.go
aws s3 cp context.tar.gz s3://my-builds-bucket/kaniko/context.tar.gz
Then point Kaniko at it:
--context=s3://my-builds-bucket/kaniko/context.tar.gz
Resource limits and build performance
Kaniko builds can be memory-intensive, especially for large base images or Go/Rust compilations. Set resource requests and limits to prevent builds from being OOM-killed or starving other workloads:
containers:
- name: kaniko
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:debug
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
args:
- --context=dir:///workspace
- --dockerfile=/workspace/Dockerfile
- --destination=docker.io/your-user/your-app:latest
The Go multi-stage build we tested consumed about 800MB of memory during compilation. Java and Rust builds typically need more (2-4GB). If your builds fail with signal: killed in the logs, increase the memory limit.
Clean up build resources
Completed Kaniko pods and jobs stick around after they finish. Clean them up to avoid cluttering your cluster:
kubectl delete job kaniko-multistage-build
kubectl delete pod kaniko-build
kubectl delete configmap kaniko-build-context
For automated cleanup, set ttlSecondsAfterFinished on the Job spec:
spec:
ttlSecondsAfterFinished: 300
backoffLimit: 0
template:
...
This tells Kubernetes to automatically delete the Job and its pod 5 minutes after completion.
Troubleshooting
Error: "error checking push permissions"
This means Kaniko cannot authenticate with the destination registry. Verify the secret is mounted correctly:
kubectl get secret docker-hub-secret -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
Confirm the auths section contains the correct registry URL. For Docker Hub, it must be https://index.docker.io/v1/ (not docker.io). Also check that the secret is mounted at /kaniko/.docker/config.json, not /kaniko/.docker/.dockerconfigjson. The items mapping in the volume mount handles this rename.
Error: "failed to get filesystem from image"
This usually happens when the base image in the Dockerfile cannot be pulled. Check that the Kaniko pod has network access to the registry and that the base image tag exists. If you are behind a corporate proxy, set the HTTP_PROXY and HTTPS_PROXY environment variables on the Kaniko container.
Build succeeds but image is too large
Use multi-stage builds. Compile in one stage, copy only the binary to a minimal base like alpine or scratch in the second stage. The Go example above produces a final image of roughly 8MB compared to the 500MB+ golang builder image.
Builds are slow on repeated runs
Enable layer caching with --cache=true. Kaniko pushes intermediate layers to the registry and checks for them on subsequent builds. This can cut rebuild times by 50-80% when only application code changes.
Kaniko vs other in-cluster image builders
| Tool | Daemon required | Privileged | Best for | Status (2026) |
|---|---|---|---|---|
| Kaniko (Chainguard fork) | No | No | Standard CI/CD, multi-tenant clusters | Actively maintained |
| BuildKit (moby/buildkit) | Yes (buildkitd) | Optional (rootless mode) | Advanced caching, distributed builds | Very active |
| Buildah | No | Needs security context | Podman/CRI-O environments | Very active |
Kaniko is the simplest option when you just need to build Dockerfiles inside Kubernetes without any special security permissions. BuildKit is more powerful but requires running a daemon (buildkitd) as a pod. Buildah is the best fit if you are already in a Podman/CRI-O ecosystem.
What port does Kaniko use?
Kaniko itself does not listen on any port. It is a build tool that runs as a one-shot process, builds the image, pushes it to a registry over HTTPS (port 443), and exits. The only network requirement is outbound HTTPS access to your container registry and any base image registries referenced in the Dockerfile.
Does Kaniko work with Podman or containerd?
Yes. Kaniko does not interact with the container runtime at all. It runs in userspace and directly talks to OCI registries over HTTPS. It works on any Kubernetes cluster regardless of whether the nodes use Docker, containerd, or CRI-O.