Kaniko Rootless Container Builds: Production Kubernetes CI/CD Guide
Learn how to implement secure, rootless container image builds using Kaniko in Kubernetes with advanced caching strategies, multi-stage build optimization, credential management, and production-ready CI/CD pipeline integration.
Kaniko Rootless Container Builds: Production Kubernetes CI/CD Guide
Executive Summary
Kaniko is Google’s open-source tool for building container images from Dockerfiles inside Kubernetes clusters without requiring Docker daemon or privileged access. This comprehensive guide covers production-ready Kaniko implementations including advanced caching strategies, multi-registry support, security hardening, and integration with popular CI/CD systems for enterprise Kubernetes environments.
Table of Contents
- Why Kaniko?
- Architecture and Components
- Basic Kaniko Setup
- Advanced Caching Strategies
- Multi-Stage Build Optimization
- Credential Management
- CI/CD Pipeline Integration
- Performance Optimization
- Security Best Practices
- Monitoring and Troubleshooting
Why Kaniko?
Traditional Docker-in-Docker Problems
# Problems with Docker-in-Docker (DinD)
issues:
security:
- Requires privileged mode
- Root access to host Docker daemon
- Potential container escape vulnerabilities
- Shared Docker daemon state
operational:
- Complex networking setup
- Volume mounting challenges
- Cache management difficulties
- Resource overhead from nested containers
compliance:
- Violates security policies
- Not suitable for multi-tenant environments
- Difficult to audit
Kaniko Advantages
# Kaniko Benefits
advantages:
security:
- No Docker daemon required
- Runs in userspace
- No privileged access needed
- Isolated per build
kubernetes_native:
- Native Kubernetes integration
- Uses standard Kubernetes primitives
- Works with RBAC and PSP/PSA
- Easy to scale
flexibility:
- Multiple registry support
- Advanced caching options
- Custom build contexts
- Reproducible builds
performance:
- Layer caching
- Parallel builds
- Efficient resource usage
Architecture and Components
Kaniko Build Process
# Kaniko Architecture
components:
executor:
description: "Main Kaniko binary that builds images"
responsibilities:
- Parse Dockerfile
- Execute build instructions
- Create image layers
- Push to registry
filesystem:
description: "Custom filesystem implementation"
features:
- Snapshot-based layer creation
- Efficient diff computation
- Whiteout file handling
cache:
description: "Layer caching system"
types:
- Local cache (emptyDir)
- Remote cache (registry)
- Inline cache metadata
build_flow:
1_initialize:
- Load build context
- Parse Dockerfile
- Authenticate to registries
2_process_stages:
- Execute each stage sequentially
- Check cache for existing layers
- Create filesystem snapshots
3_push_image:
- Tag final image
- Push layers to registry
- Update cache metadata
Basic Kaniko Setup
Simple Kaniko Build Pod
# simple-kaniko-build.yaml
apiVersion: v1
kind: Pod
metadata:
name: kaniko-build
namespace: ci-cd
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git#refs/heads/main"
- "--destination=gcr.io/my-project/myapp:latest"
- "--cache=true"
- "--cache-ttl=24h"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
readOnly: true
resources:
requests:
cpu: 1000m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
items:
- key: .dockerconfigjson
path: config.json
Registry Credentials Secret
#!/bin/bash
# create-registry-secrets.sh
set -euo pipefail
export NAMESPACE="ci-cd"
# Google Container Registry (GCR)
kubectl create secret docker-registry gcr-credentials \
-n ${NAMESPACE} \
--docker-server=gcr.io \
--docker-username=_json_key \
--docker-password="$(cat ~/gcp-key.json)" \
--docker-email=ci@example.com
# Amazon ECR
kubectl create secret docker-registry ecr-credentials \
-n ${NAMESPACE} \
--docker-server=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com \
--docker-username=AWS \
--docker-password="$(aws ecr get-login-password --region ${AWS_REGION})" \
--docker-email=ci@example.com
# Docker Hub
kubectl create secret docker-registry dockerhub-credentials \
-n ${NAMESPACE} \
--docker-server=https://index.docker.io/v1/ \
--docker-username=${DOCKERHUB_USERNAME} \
--docker-password=${DOCKERHUB_PASSWORD} \
--docker-email=ci@example.com
# Combined config for multiple registries
cat > docker-config.json <<EOF
{
"auths": {
"gcr.io": {
"auth": "$(echo -n "_json_key:$(cat ~/gcp-key.json)" | base64 -w0)"
},
"${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com": {
"auth": "$(echo -n "AWS:$(aws ecr get-login-password --region ${AWS_REGION})" | base64 -w0)"
},
"https://index.docker.io/v1/": {
"auth": "$(echo -n "${DOCKERHUB_USERNAME}:${DOCKERHUB_PASSWORD}" | base64 -w0)"
}
}
}
EOF
kubectl create secret generic docker-registry-config \
-n ${NAMESPACE} \
--from-file=.dockerconfigjson=docker-config.json \
--type=kubernetes.io/dockerconfigjson
rm docker-config.json
Dockerfile for Multi-Stage Build
# Dockerfile - Production-ready multi-stage build
# syntax=docker/dockerfile:1.4
# Stage 1: Build dependencies
FROM golang:1.21-alpine AS builder
# Install build dependencies
RUN apk add --no-cache \
git \
ca-certificates \
tzdata
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Build application
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o /app/server ./cmd/server
# Stage 2: Runtime
FROM gcr.io/distroless/static-debian11:nonroot
# Copy timezone and CA certificates
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy application binary
COPY --from=builder /app/server /app/server
# Use non-root user
USER nonroot:nonroot
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app/server", "healthcheck"]
EXPOSE 8080
ENTRYPOINT ["/app/server"]
Advanced Caching Strategies
Registry-Based Cache
# kaniko-with-registry-cache.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-build-cached
namespace: ci-cd
spec:
template:
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
# Source configuration
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git#refs/heads/main"
- "--git=branch=main"
# Destination
- "--destination=gcr.io/my-project/myapp:$(git rev-parse --short HEAD)"
- "--destination=gcr.io/my-project/myapp:latest"
# Registry cache configuration
- "--cache=true"
- "--cache-ttl=168h" # 1 week
- "--cache-repo=gcr.io/my-project/cache"
- "--cache-copy-layers=true"
# Build optimizations
- "--compressed-caching=true"
- "--use-new-run=true"
- "--snapshot-mode=redo"
# Reproducible builds
- "--reproducible=true"
- "--cleanup=true"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
- name: cache-volume
mountPath: /cache
resources:
requests:
cpu: 2000m
memory: 4Gi
limits:
cpu: 4000m
memory: 8Gi
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
- name: cache-volume
emptyDir:
sizeLimit: 10Gi
Persistent Volume Cache
# kaniko-pvc-cache.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: kaniko-cache
namespace: ci-cd
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs-client
resources:
requests:
storage: 100Gi
---
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-build-pvc-cache
namespace: ci-cd
spec:
template:
spec:
restartPolicy: Never
initContainers:
# Pre-warm cache if empty
- name: cache-warmer
image: gcr.io/kaniko-project/warmer:v1.15.0
args:
- "--cache-dir=/cache"
- "--image=golang:1.21-alpine"
- "--image=gcr.io/distroless/static-debian11:nonroot"
volumeMounts:
- name: kaniko-cache
mountPath: /cache
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=gcr.io/my-project/myapp:latest"
- "--cache=true"
- "--cache-dir=/cache"
- "--cache-ttl=336h" # 2 weeks
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
- name: kaniko-cache
mountPath: /cache
resources:
requests:
cpu: 2000m
memory: 4Gi
limits:
cpu: 4000m
memory: 8Gi
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
- name: kaniko-cache
persistentVolumeClaim:
claimName: kaniko-cache
Cache Warming Strategy
#!/bin/bash
# warm-kaniko-cache.sh - Pre-populate cache with common base images
set -euo pipefail
export CACHE_REGISTRY="gcr.io/my-project/cache"
export NAMESPACE="ci-cd"
# Common base images to cache
BASE_IMAGES=(
"golang:1.21-alpine"
"node:20-alpine"
"python:3.11-slim"
"gcr.io/distroless/static-debian11:nonroot"
"gcr.io/distroless/base-debian11:nonroot"
"nginx:1.25-alpine"
"redis:7-alpine"
)
echo "Warming cache with common base images..."
for image in "${BASE_IMAGES[@]}"; do
echo "Caching ${image}..."
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: cache-warmer-$(echo ${image} | tr ':/' '-' | tr '.' '-')
namespace: ${NAMESPACE}
spec:
ttlSecondsAfterFinished: 100
template:
spec:
restartPolicy: Never
containers:
- name: warmer
image: gcr.io/kaniko-project/warmer:v1.15.0
args:
- "--cache-dir=/cache"
- "--image=${image}"
volumeMounts:
- name: kaniko-cache
mountPath: /cache
- name: docker-config
mountPath: /kaniko/.docker/
volumes:
- name: kaniko-cache
persistentVolumeClaim:
claimName: kaniko-cache
- name: docker-config
secret:
secretName: docker-registry-config
EOF
sleep 2
done
echo "Cache warming jobs created"
Multi-Stage Build Optimization
Optimized Multi-Stage Dockerfile
# Dockerfile.optimized - Advanced multi-stage optimization
# syntax=docker/dockerfile:1.4
# Global ARGs
ARG GO_VERSION=1.21
ARG ALPINE_VERSION=3.18
ARG DISTROLESS_VERSION=nonroot
################################
# Stage 1: Dependency builder
################################
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS deps
RUN apk add --no-cache git ca-certificates
WORKDIR /src
# Copy dependency files only
COPY go.mod go.sum ./
# Download dependencies with cache mount
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download && \
go mod verify
################################
# Stage 2: Builder
################################
FROM deps AS builder
# Copy source code
COPY . .
# Build with cache mounts
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64 \
go build \
-ldflags="-w -s -X main.version=$(git describe --tags --always)" \
-o /out/server \
./cmd/server
# Strip binary
RUN apk add --no-cache upx && \
upx --best --lzma /out/server
################################
# Stage 3: Testing (optional)
################################
FROM builder AS tester
# Run tests
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go test -v -race -coverprofile=coverage.out ./...
################################
# Stage 4: Runtime
################################
FROM gcr.io/distroless/static-debian11:${DISTROLESS_VERSION} AS runtime
# Metadata
LABEL maintainer="devops@example.com" \
org.opencontainers.image.source="https://github.com/myorg/myapp" \
org.opencontainers.image.description="Production application" \
org.opencontainers.image.licenses="MIT"
# Copy CA certificates
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy application
COPY --from=builder --chown=nonroot:nonroot /out/server /app/server
# Non-root user
USER nonroot:nonroot
WORKDIR /app
EXPOSE 8080 9090
ENTRYPOINT ["/app/server"]
Kaniko Build with BuildKit Features
# kaniko-buildkit-features.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-buildkit
namespace: ci-cd
spec:
template:
metadata:
labels:
app: kaniko-build
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
# Build configuration
- "--dockerfile=Dockerfile.optimized"
- "--context=dir:///workspace"
- "--destination=gcr.io/my-project/myapp:${GIT_COMMIT}"
# BuildKit features
- "--use-new-run=true"
- "--enable-buildkit=true"
- "--build-arg=GO_VERSION=1.21"
- "--build-arg=BUILDKIT_INLINE_CACHE=1"
# Cache configuration
- "--cache=true"
- "--cache-repo=gcr.io/my-project/cache"
- "--compressed-caching=true"
# Target specific stage
- "--target=runtime"
# Performance options
- "--single-snapshot=true"
- "--snapshot-mode=redo"
- "--push-retry=3"
# Security
- "--skip-tls-verify=false"
- "--insecure=false"
volumeMounts:
- name: workspace
mountPath: /workspace
- name: docker-config
mountPath: /kaniko/.docker/
env:
- name: GIT_COMMIT
value: "abc123"
resources:
requests:
cpu: 2000m
memory: 4Gi
ephemeral-storage: 10Gi
limits:
cpu: 4000m
memory: 8Gi
ephemeral-storage: 20Gi
initContainers:
- name: git-clone
image: alpine/git:latest
command:
- sh
- -c
- |
git clone --depth=1 https://github.com/myorg/myapp.git /workspace
cd /workspace
git fetch --depth=1 origin ${GIT_REF}
git checkout ${GIT_REF}
env:
- name: GIT_REF
value: "main"
volumeMounts:
- name: workspace
mountPath: /workspace
volumes:
- name: workspace
emptyDir:
sizeLimit: 10Gi
- name: docker-config
secret:
secretName: docker-registry-config
Credential Management
AWS ECR Integration
#!/bin/bash
# kaniko-ecr-auth.sh - Automated ECR authentication
set -euo pipefail
export AWS_REGION="${AWS_REGION:-us-east-1}"
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export NAMESPACE="ci-cd"
# Get ECR login token
ECR_PASSWORD=$(aws ecr get-login-password --region ${AWS_REGION})
# Create Kubernetes secret
kubectl create secret docker-registry ecr-credentials \
-n ${NAMESPACE} \
--docker-server=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com \
--docker-username=AWS \
--docker-password="${ECR_PASSWORD}" \
--docker-email=ecr@example.com \
--dry-run=client -o yaml | kubectl apply -f -
# Create CronJob to refresh token (valid for 12 hours)
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: CronJob
metadata:
name: ecr-token-refresh
namespace: ${NAMESPACE}
spec:
schedule: "0 */6 * * *" # Every 6 hours
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
serviceAccountName: ecr-token-manager
restartPolicy: OnFailure
containers:
- name: token-refresh
image: amazon/aws-cli:latest
command:
- /bin/sh
- -c
- |
ECR_PASSWORD=\$(aws ecr get-login-password --region ${AWS_REGION})
kubectl create secret docker-registry ecr-credentials \
-n ${NAMESPACE} \
--docker-server=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com \
--docker-username=AWS \
--docker-password="\${ECR_PASSWORD}" \
--docker-email=ecr@example.com \
--dry-run=client -o yaml | kubectl apply -f -
env:
- name: AWS_REGION
value: "${AWS_REGION}"
EOF
GCR Service Account Integration
# gcr-workload-identity.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: kaniko-builder
namespace: ci-cd
annotations:
iam.gke.io/gcp-service-account: kaniko-builder@my-project.iam.gserviceaccount.com
---
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-gcr-workload-identity
namespace: ci-cd
spec:
template:
spec:
serviceAccountName: kaniko-builder
restartPolicy: Never
initContainers:
# Generate GCR credentials using Workload Identity
- name: gcr-auth
image: google/cloud-sdk:alpine
command:
- sh
- -c
- |
gcloud auth application-default print-access-token | \
docker-credential-gcr gcr-login
cat > /kaniko/.docker/config.json <<EOF
{
"credHelpers": {
"gcr.io": "gcr",
"us.gcr.io": "gcr",
"eu.gcr.io": "gcr",
"asia.gcr.io": "gcr"
}
}
EOF
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=gcr.io/my-project/myapp:latest"
- "--cache=true"
- "--cache-repo=gcr.io/my-project/cache"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
volumes:
- name: docker-config
emptyDir: {}
Private Registry with Custom CA
# kaniko-custom-ca.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: custom-ca-certs
namespace: ci-cd
data:
ca-bundle.crt: |
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKJ... (your custom CA cert)
-----END CERTIFICATE-----
---
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-custom-registry
namespace: ci-cd
spec:
template:
spec:
restartPolicy: Never
initContainers:
- name: setup-ca
image: alpine:latest
command:
- sh
- -c
- |
cp /custom-ca/ca-bundle.crt /usr/local/share/ca-certificates/
update-ca-certificates
cp /etc/ssl/certs/ca-certificates.crt /shared-ca/
volumeMounts:
- name: custom-ca
mountPath: /custom-ca
- name: shared-ca
mountPath: /shared-ca
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=registry.internal.example.com/myapp:latest"
- "--registry-certificate=registry.internal.example.com=/kaniko/ssl/certs/ca-bundle.crt"
- "--skip-tls-verify-registry=registry.internal.example.com"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
- name: custom-ca
mountPath: /kaniko/ssl/certs
- name: shared-ca
mountPath: /etc/ssl/certs
subPath: ca-certificates.crt
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
- name: custom-ca
configMap:
name: custom-ca-certs
- name: shared-ca
emptyDir: {}
CI/CD Pipeline Integration
Tekton Pipeline Integration
# tekton-kaniko-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: build-and-deploy
namespace: ci-cd
spec:
params:
- name: git-url
type: string
- name: git-revision
type: string
default: main
- name: image-name
type: string
- name: image-tag
type: string
workspaces:
- name: shared-workspace
- name: docker-credentials
tasks:
- name: fetch-repository
taskRef:
name: git-clone
workspaces:
- name: output
workspace: shared-workspace
params:
- name: url
value: $(params.git-url)
- name: revision
value: $(params.git-revision)
- name: run-tests
runAfter: [fetch-repository]
taskRef:
name: golang-test
workspaces:
- name: source
workspace: shared-workspace
- name: build-image
runAfter: [run-tests]
taskRef:
name: kaniko
workspaces:
- name: source
workspace: shared-workspace
- name: dockerconfig
workspace: docker-credentials
params:
- name: IMAGE
value: $(params.image-name):$(params.image-tag)
- name: EXTRA_ARGS
value:
- --cache=true
- --cache-ttl=24h
- --compressed-caching=true
- --snapshot-mode=redo
- --use-new-run=true
- name: security-scan
runAfter: [build-image]
taskRef:
name: trivy-scanner
params:
- name: IMAGE
value: $(params.image-name):$(params.image-tag)
- name: deploy-to-staging
runAfter: [security-scan]
taskRef:
name: kubectl-deploy
params:
- name: IMAGE
value: $(params.image-name):$(params.image-tag)
- name: NAMESPACE
value: staging
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: kaniko
namespace: ci-cd
spec:
params:
- name: IMAGE
description: Name (reference) of the image to build
- name: DOCKERFILE
description: Path to the Dockerfile to build
default: ./Dockerfile
- name: CONTEXT
description: The build context used by Kaniko
default: ./
- name: EXTRA_ARGS
type: array
default: []
workspaces:
- name: source
- name: dockerconfig
mountPath: /kaniko/.docker
results:
- name: IMAGE_DIGEST
description: Digest of the image just built
steps:
- name: build-and-push
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- --dockerfile=$(params.DOCKERFILE)
- --context=$(workspaces.source.path)/$(params.CONTEXT)
- --destination=$(params.IMAGE)
- --digest-file=$(results.IMAGE_DIGEST.path)
- $(params.EXTRA_ARGS[*])
securityContext:
runAsUser: 0
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
GitLab CI Integration
# .gitlab-ci.yml
variables:
DOCKER_REGISTRY: gcr.io
IMAGE_NAME: ${DOCKER_REGISTRY}/${GCP_PROJECT}/${CI_PROJECT_NAME}
KANIKO_IMAGE: gcr.io/kaniko-project/executor:v1.15.0
KANIKO_CACHE_REPO: ${DOCKER_REGISTRY}/${GCP_PROJECT}/cache
stages:
- build
- test
- deploy
.kaniko-build:
image:
name: ${KANIKO_IMAGE}
entrypoint: [""]
before_script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${DOCKER_REGISTRY}\":{\"auth\":\"$(echo -n _json_key:${GCP_SA_KEY} | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
build:development:
extends: .kaniko-build
stage: build
script:
- /kaniko/executor
--context ${CI_PROJECT_DIR}
--dockerfile ${CI_PROJECT_DIR}/Dockerfile
--destination ${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}
--destination ${IMAGE_NAME}:${CI_COMMIT_REF_SLUG}
--cache=true
--cache-repo=${KANIKO_CACHE_REPO}
--cache-ttl=168h
--compressed-caching=true
--snapshot-mode=redo
--use-new-run=true
--build-arg BUILDKIT_INLINE_CACHE=1
--label "git.commit=${CI_COMMIT_SHA}"
--label "git.branch=${CI_COMMIT_REF_NAME}"
--label "pipeline.id=${CI_PIPELINE_ID}"
only:
- branches
except:
- main
build:production:
extends: .kaniko-build
stage: build
script:
- /kaniko/executor
--context ${CI_PROJECT_DIR}
--dockerfile ${CI_PROJECT_DIR}/Dockerfile
--destination ${IMAGE_NAME}:${CI_COMMIT_TAG}
--destination ${IMAGE_NAME}:latest
--cache=true
--cache-repo=${KANIKO_CACHE_REPO}
--cache-ttl=336h
--compressed-caching=true
--snapshot-mode=redo
--use-new-run=true
--reproducible=true
--label "git.tag=${CI_COMMIT_TAG}"
--label "pipeline.id=${CI_PIPELINE_ID}"
only:
- tags
test:security-scan:
stage: test
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL ${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA}
dependencies:
- build:development
deploy:staging:
stage: deploy
image: google/cloud-sdk:alpine
script:
- kubectl set image deployment/myapp myapp=${IMAGE_NAME}:${CI_COMMIT_SHORT_SHA} -n staging
- kubectl rollout status deployment/myapp -n staging
only:
- main
dependencies:
- build:development
Jenkins Pipeline
// Jenkinsfile - Kaniko build pipeline
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins: agent
spec:
serviceAccountName: jenkins-agent
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0-debug
command:
- /busybox/cat
tty: true
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
resources:
requests:
cpu: 2000m
memory: 4Gi
limits:
cpu: 4000m
memory: 8Gi
- name: kubectl
image: bitnami/kubectl:latest
command:
- cat
tty: true
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
"""
}
}
environment {
GIT_COMMIT_SHORT = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
IMAGE_TAG = "${env.BRANCH_NAME}-${GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
IMAGE_NAME = "gcr.io/my-project/myapp"
CACHE_REPO = "gcr.io/my-project/cache"
}
stages {
stage('Build Image') {
steps {
container('kaniko') {
sh """
/kaniko/executor \
--context \${WORKSPACE} \
--dockerfile \${WORKSPACE}/Dockerfile \
--destination ${IMAGE_NAME}:${IMAGE_TAG} \
--destination ${IMAGE_NAME}:${env.BRANCH_NAME}-latest \
--cache=true \
--cache-repo=${CACHE_REPO} \
--cache-ttl=168h \
--compressed-caching=true \
--snapshot-mode=redo \
--use-new-run=true \
--build-arg BUILD_NUMBER=${env.BUILD_NUMBER} \
--build-arg GIT_COMMIT=${env.GIT_COMMIT} \
--label 'jenkins.build=${env.BUILD_NUMBER}' \
--label 'git.commit=${env.GIT_COMMIT}'
"""
}
}
}
stage('Security Scan') {
steps {
sh """
docker run --rm \
aquasec/trivy:latest \
image --severity HIGH,CRITICAL \
${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
stage('Deploy to Staging') {
when {
branch 'main'
}
steps {
container('kubectl') {
sh """
kubectl set image deployment/myapp \
myapp=${IMAGE_NAME}:${IMAGE_TAG} \
-n staging
kubectl rollout status deployment/myapp \
-n staging \
--timeout=5m
"""
}
}
}
stage('Integration Tests') {
when {
branch 'main'
}
steps {
sh './run-integration-tests.sh staging'
}
}
stage('Promote to Production') {
when {
branch 'main'
}
input {
message 'Deploy to production?'
ok 'Deploy'
}
steps {
container('kubectl') {
sh """
kubectl set image deployment/myapp \
myapp=${IMAGE_NAME}:${IMAGE_TAG} \
-n production
kubectl rollout status deployment/myapp \
-n production \
--timeout=10m
"""
}
}
}
}
post {
always {
cleanWs()
}
success {
slackSend(
color: 'good',
message: "Build ${env.BUILD_NUMBER} succeeded: ${IMAGE_NAME}:${IMAGE_TAG}"
)
}
failure {
slackSend(
color: 'danger',
message: "Build ${env.BUILD_NUMBER} failed"
)
}
}
}
Performance Optimization
Resource Optimization
# kaniko-performance-optimized.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-optimized
namespace: ci-cd
spec:
parallelism: 1
completions: 1
backoffLimit: 2
template:
spec:
restartPolicy: Never
# Node affinity for build nodes
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload-type
operator: In
values:
- build
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- kaniko-build
topologyKey: kubernetes.io/hostname
# Priority for build workloads
priorityClassName: build-priority
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=gcr.io/my-project/myapp:latest"
# Performance optimizations
- "--cache=true"
- "--cache-repo=gcr.io/my-project/cache"
- "--cache-ttl=336h"
- "--compressed-caching=true"
- "--single-snapshot=true"
- "--snapshot-mode=redo"
- "--use-new-run=true"
- "--push-retry=3"
- "--registry-mirror=mirror.gcr.io"
# Resource limits
resources:
requests:
cpu: 4000m
memory: 8Gi
ephemeral-storage: 20Gi
limits:
cpu: 8000m
memory: 16Gi
ephemeral-storage: 50Gi
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
- name: cache
mountPath: /cache
# Lifecycle management
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
- name: cache
emptyDir:
sizeLimit: 30Gi
# Tolerations for build nodes
tolerations:
- key: "workload-type"
operator: "Equal"
value: "build"
effect: "NoSchedule"
Parallel Multi-Architecture Builds
# kaniko-multi-arch-parallel.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-multi-arch-amd64
namespace: ci-cd
spec:
template:
spec:
restartPolicy: Never
nodeSelector:
kubernetes.io/arch: amd64
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=gcr.io/my-project/myapp:latest-amd64"
- "--cache=true"
- "--cache-repo=gcr.io/my-project/cache"
- "--custom-platform=linux/amd64"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
---
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-multi-arch-arm64
namespace: ci-cd
spec:
template:
spec:
restartPolicy: Never
nodeSelector:
kubernetes.io/arch: arm64
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=gcr.io/my-project/myapp:latest-arm64"
- "--cache=true"
- "--cache-repo=gcr.io/my-project/cache"
- "--custom-platform=linux/arm64"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
---
# Manifest creation job
apiVersion: batch/v1
kind: Job
metadata:
name: create-multi-arch-manifest
namespace: ci-cd
spec:
template:
spec:
restartPolicy: Never
containers:
- name: manifest-tool
image: mplatform/manifest-tool:alpine
command:
- sh
- -c
- |
manifest-tool push from-args \
--platforms linux/amd64,linux/arm64 \
--template gcr.io/my-project/myapp:latest-ARCH \
--target gcr.io/my-project/myapp:latest
volumeMounts:
- name: docker-config
mountPath: /root/.docker/
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
Security Best Practices
Pod Security Standards
# kaniko-security-hardened.yaml
apiVersion: v1
kind: Namespace
metadata:
name: ci-cd-secure
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kaniko-secure
namespace: ci-cd-secure
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kaniko-minimal
namespace: ci-cd-secure
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["docker-registry-config"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kaniko-secure-binding
namespace: ci-cd-secure
subjects:
- kind: ServiceAccount
name: kaniko-secure
roleRef:
kind: Role
name: kaniko-minimal
apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-secure-build
namespace: ci-cd-secure
spec:
template:
metadata:
labels:
app: kaniko-build
annotations:
container.apparmor.security.beta.kubernetes.io/kaniko: runtime/default
spec:
serviceAccountName: kaniko-secure
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=gcr.io/my-project/myapp:latest"
- "--cache=true"
- "--skip-tls-verify=false"
- "--insecure=false"
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
readOnly: true
- name: tmp
mountPath: /tmp
- name: kaniko-secret
mountPath: /kaniko
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
defaultMode: 0400
- name: tmp
emptyDir: {}
- name: kaniko-secret
emptyDir:
medium: Memory
Image Scanning Integration
#!/bin/bash
# kaniko-with-scanning.sh - Build with integrated security scanning
set -euo pipefail
export IMAGE_NAME="gcr.io/my-project/myapp"
export IMAGE_TAG="$(git rev-parse --short HEAD)"
export NAMESPACE="ci-cd"
# Create build job
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-build-${IMAGE_TAG}
namespace: ${NAMESPACE}
spec:
template:
spec:
restartPolicy: Never
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.15.0
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/myorg/myapp.git"
- "--destination=${IMAGE_NAME}:${IMAGE_TAG}"
- "--destination=${IMAGE_NAME}:latest"
- "--cache=true"
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
EOF
# Wait for build to complete
kubectl wait --for=condition=complete job/kaniko-build-${IMAGE_TAG} \
-n ${NAMESPACE} --timeout=600s
# Run Trivy scan
echo "Running security scan..."
trivy image \
--severity HIGH,CRITICAL \
--exit-code 1 \
--format json \
--output trivy-report.json \
${IMAGE_NAME}:${IMAGE_TAG}
# Run Grype scan
echo "Running vulnerability scan..."
grype ${IMAGE_NAME}:${IMAGE_TAG} \
--fail-on high \
--output json \
--file grype-report.json
# Check for secrets
echo "Scanning for secrets..."
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
trufflesecurity/trufflehog:latest \
docker --image ${IMAGE_NAME}:${IMAGE_TAG}
echo "Build and security scanning complete"
Monitoring and Troubleshooting
Monitoring Dashboard
# kaniko-monitoring.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: kaniko-dashboard
namespace: monitoring
data:
kaniko-builds.json: |
{
"dashboard": {
"title": "Kaniko Builds",
"panels": [
{
"title": "Build Success Rate",
"targets": [
{
"expr": "sum(kube_job_status_succeeded{namespace=\"ci-cd\",job_name=~\"kaniko-.*\"}) / sum(kube_job_status_succeeded{namespace=\"ci-cd\",job_name=~\"kaniko-.*\"} or kube_job_status_failed{namespace=\"ci-cd\",job_name=~\"kaniko-.*\"})"
}
]
},
{
"title": "Build Duration P95",
"targets": [
{
"expr": "histogram_quantile(0.95, sum(rate(kube_job_complete_duration_seconds_bucket{namespace=\"ci-cd\",job_name=~\"kaniko-.*\"}[5m])) by (le))"
}
]
},
{
"title": "Resource Usage",
"targets": [
{
"expr": "sum(container_memory_working_set_bytes{namespace=\"ci-cd\",pod=~\"kaniko-.*\"}) by (pod)"
}
]
},
{
"title": "Cache Hit Rate",
"targets": [
{
"expr": "sum(rate(kaniko_cache_hits_total[5m])) / (sum(rate(kaniko_cache_hits_total[5m])) + sum(rate(kaniko_cache_misses_total[5m])))"
}
]
}
]
}
}
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: kaniko-alerts
namespace: ci-cd
spec:
groups:
- name: kaniko
interval: 30s
rules:
- alert: KanikoBuilFailureRate
expr: |
sum(rate(kube_job_status_failed{namespace="ci-cd",job_name=~"kaniko-.*"}[5m]))
/
sum(rate(kube_job_complete{namespace="ci-cd",job_name=~"kaniko-.*"}[5m]))
> 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "High Kaniko build failure rate"
description: "Build failure rate is {{ $value | humanizePercentage }}"
- alert: KanikoBuildDurationHigh
expr: |
histogram_quantile(0.95,
sum(rate(kube_job_complete_duration_seconds_bucket{namespace="ci-cd",job_name=~"kaniko-.*"}[5m])) by (le)
) > 600
for: 10m
labels:
severity: warning
annotations:
summary: "Slow Kaniko builds"
description: "P95 build duration is {{ $value }}s"
- alert: KanikoCachePerformance
expr: |
sum(rate(kaniko_cache_hits_total[5m]))
/
(sum(rate(kaniko_cache_hits_total[5m])) + sum(rate(kaniko_cache_misses_total[5m])))
< 0.5
for: 15m
labels:
severity: info
annotations:
summary: "Low Kaniko cache hit rate"
description: "Cache hit rate is {{ $value | humanizePercentage }}"
Troubleshooting Guide
#!/bin/bash
# troubleshoot-kaniko.sh - Debug Kaniko build issues
set -euo pipefail
export POD_NAME="${1:-}"
export NAMESPACE="${2:-ci-cd}"
if [ -z "$POD_NAME" ]; then
echo "Usage: $0 <pod-name> [namespace]"
exit 1
fi
echo "=== Kaniko Build Troubleshooting ==="
echo "Pod: $POD_NAME"
echo "Namespace: $NAMESPACE"
echo
# Check pod status
echo "--- Pod Status ---"
kubectl get pod $POD_NAME -n $NAMESPACE
# Check events
echo -e "\n--- Recent Events ---"
kubectl get events -n $NAMESPACE \
--field-selector involvedObject.name=$POD_NAME \
--sort-by='.lastTimestamp'
# Check logs
echo -e "\n--- Container Logs ---"
kubectl logs $POD_NAME -n $NAMESPACE
# Check resource usage
echo -e "\n--- Resource Usage ---"
kubectl top pod $POD_NAME -n $NAMESPACE
# Check volume mounts
echo -e "\n--- Volume Mounts ---"
kubectl describe pod $POD_NAME -n $NAMESPACE | grep -A 10 "Mounts:"
# Check docker config secret
echo -e "\n--- Docker Config Secret ---"
kubectl get secret docker-registry-config -n $NAMESPACE -o yaml
# Debug mode execution
echo -e "\n--- Running Debug Build ---"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: ${POD_NAME}-debug
namespace: ${NAMESPACE}
spec:
restartPolicy: Never
containers:
- name: kaniko-debug
image: gcr.io/kaniko-project/executor:v1.15.0-debug
command: ["/busybox/sh"]
args: ["-c", "sleep 3600"]
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker/
- name: workspace
mountPath: /workspace
volumes:
- name: docker-config
secret:
secretName: docker-registry-config
- name: workspace
emptyDir: {}
EOF
echo "Debug pod created. Execute commands with:"
echo "kubectl exec -it ${POD_NAME}-debug -n ${NAMESPACE} -- /busybox/sh"
Conclusion
Kaniko provides a secure, Kubernetes-native solution for building container images without Docker daemon dependencies. This guide has covered production-ready implementations including advanced caching strategies, multi-registry support, CI/CD integration, and comprehensive security best practices.
Key takeaways:
- Security: Kaniko runs without privileged access, making it suitable for multi-tenant environments
- Performance: Advanced caching strategies significantly improve build times
- Integration: Native Kubernetes integration works seamlessly with CI/CD pipelines
- Flexibility: Supports multiple registries, custom CAs, and complex authentication scenarios
- Scalability: Horizontal scaling with parallel builds and resource optimization
- Monitoring: Comprehensive observability for build performance and success rates
For more information on CI/CD and container workflows, see our guides on Buildpacks vs Dockerfile comparison and Container security scanning.