Article
Kaniko is a Google-developed tool for building container images from a Dockerfile inside containers or Kubernetes clusters. Unlike traditional Docker builds, Kaniko doesn't require a Docker daemon, making it ideal for secure, reproducible container builds in CI/CD pipelines and Kubernetes environments.
For Java teams, Kaniko provides a secure way to build container images without privileged access, enabling true container-native builds in cloud environments.
Why Kaniko for Java Container Builds?
- Daemonless Architecture: No Docker daemon required - perfect for Kubernetes and restricted environments
- Security-First: Runs without root privileges, eliminating security concerns of Docker-in-Docker
- Reproducible Builds: Consistent image builds across different environments
- Layer Caching: Efficient caching mechanisms for faster Java builds
- Kubernetes Native: Designed to run natively in Kubernetes clusters
- Multi-Architecture Support: Build images for multiple platforms (amd64, arm64)
Part 1: Kaniko Fundamentals for Java
1.1 How Kaniko Works with Java Applications
Kaniko executes each command in the Dockerfile in userspace, taking a Dockerfile and build context, and pushing the built image to a registry. For Java applications, this means:
- Dependency Resolution: Maven/Gradle dependencies are cached efficiently
- Multi-Stage Builds: Perfect for separating build and runtime environments
- JAR Optimization: Optimized layer caching for application JARs
- Security Scanning: Integration with security tools during build process
1.2 Project Structure
java-kaniko-app/ ├── .codefresh/ │ └── kaniko-pipeline.yml ├── src/ │ └── main/java/com/example/ ├── Dockerfile ├── Dockerfile.kaniko ├── .dockerignore ├── pom.xml ├── build.gradle └── kaniko-config/ └── kaniko-secrets.yaml
Part 2: Dockerfile Optimization for Kaniko
2.1 Multi-Stage Dockerfile for Java
# Dockerfile # Build stage - Maven with dependencies caching FROM maven:3.8.6-openjdk-17 AS builder WORKDIR /app # Copy pom.xml first for better layer caching COPY pom.xml . COPY .mvn/ .mvn/ COPY mvnw . # Download dependencies (this layer is cached separately) RUN mvn dependency:go-offline -B # Copy source code COPY src ./src # Build application RUN mvn clean package -DskipTests # Runtime stage FROM eclipse-temurin:17-jre-jammy WORKDIR /app # Install security updates and required packages RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # Create non-root user RUN groupadd -r spring && useradd -r -g spring spring # Copy JAR from builder stage COPY --from=builder /app/target/*.jar app.jar COPY --from=builder /app/target/classes /app/classes # Change to non-root user USER spring # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]
2.2 Kaniko-Optimized Dockerfile
# Dockerfile.kaniko # Optimized for Kaniko builds with better caching # Stage 1: Dependency resolution FROM maven:3.8.6-openjdk-17 AS deps WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B # Stage 2: Test compilation FROM deps AS test-compile COPY src ./src RUN mvn test-compile -B # Stage 3: Package application FROM deps AS package COPY src ./src RUN mvn clean package -DskipTests -B # Stage 4: Runtime FROM eclipse-temurin:17-jre-jammy AS runtime # Security hardening RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/* && \ groupadd -r spring && useradd -r -g spring spring WORKDIR /app USER spring COPY --from=package /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]
2.3 .dockerignore for Java
# .dockerignore .git .gitignore README.md **/target/ **/build/ **/.gradle/ **/.mvn/wrapper/ **/mvnw **/mvnw.cmd **/gradlew **/gradlew.bat **/*.iml **/.idea/ **/*.log **/docker-compose.yml **/Dockerfile* **/.dockerignore **/node_modules/ **/npm-debug.log* **/yarn-debug.log* **/yarn-error.log*
Part 3: Kaniko in CI/CD Pipelines
3.1 Codefresh Pipeline with Kaniko
# .codefresh/kaniko-pipeline.yml
version: '1.0'
env:
APP_NAME: 'my-java-app'
REGISTRY: 'docker.io'
REPOSITORY: 'myorg'
KANIKO_IMAGE: 'gcr.io/kaniko-project/executor:latest'
MAVEN_IMAGE: 'maven:3.8.6-openjdk-17'
stages:
- prepare
- test
- build_container
- security_scan
- deploy
steps:
clone:
title: 'Clone Repository'
type: 'git-clone'
repo: '${{CF_REPO_OWNER}}/${{CF_REPO_NAME}}'
revision: '${{CF_REVISION}}'
stage: prepare
unit_tests:
title: 'Run Unit Tests'
stage: test
image: '${{MAVEN_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- mvn clean test
- mvn jacoco:report
environment:
- MAVEN_OPTS=-Dmaven.repo.local=/codefresh/volume/m2_repository
build_with_kaniko:
title: 'Build Container with Kaniko'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- /kaniko/executor
--context=.
--dockerfile=Dockerfile
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:latest
--cache=true
--cache-ttl=72h
--cache-repo=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}-cache
--build-arg=BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
--build-arg=VERSION=${{CF_SHORT_REVISION}}
environment:
- DOCKER_CONFIG=/kaniko/.docker
when:
condition:
all:
branchMaster: '"${{CF_BRANCH}}" == "master"'
notPR: '${{CF_PULL_REQUEST}}' == ''
security_scan:
title: 'Container Security Scan'
stage: security_scan
image: 'aquasec/trivy:latest'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- trivy image --exit-code 1 --severity HIGH,CRITICAL ${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}
when:
condition:
branchMaster: '"${{CF_BRANCH}}" == "master"'
3.2 Advanced Kaniko Pipeline with Caching
# .codefresh/kaniko-advanced.yml
version: '1.0'
steps:
kaniko_build_with_advanced_caching:
title: 'Kaniko Build with Layer Caching'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- |
/kaniko/executor \
--context=/codefresh/volume/app \
--dockerfile=Dockerfile \
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}} \
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_BRANCH}} \
--cache=true \
--cache-dir=/cache \
--cache-run-layers=true \
--cache-copy-layers=true \
--cache-ttl=168h \
--snapshotMode=redo \
--use-new-run \
--build-arg=MAVEN_IMAGE=${{MAVEN_IMAGE}} \
--build-arg=JAVA_VERSION=17 \
--build-arg=APP_VERSION=${{CF_SHORT_REVISION}} \
--label=org.label-schema.build-date=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--label=org.label-schema.vcs-ref=${{CF_REVISION}} \
--label=org.label-schema.version=${{CF_SHORT_REVISION}}
environment:
- DOCKER_CONFIG=/kaniko/.docker
Part 4: Kubernetes Native Kaniko Builds
4.1 Kaniko Pod Specification for Java
# kubernetes/kaniko-pod.yaml apiVersion: v1 kind: Pod metadata: name: kaniko-build-java-app namespace: build-system spec: containers: - name: kaniko image: gcr.io/kaniko-project/executor:latest args: - --dockerfile=Dockerfile - --context=git://github.com/myorg/my-java-app.git - --destination=docker.io/myorg/my-java-app:latest - --cache=true - --cache-ttl=72h - --cache-repo=docker.io/myorg/cache - --build-arg=MAVEN_IMAGE=maven:3.8.6-openjdk-17 - --verbosity=info - --skip-tls-verify=false volumeMounts: - name: kaniko-secret mountPath: /kaniko/.docker - name: maven-cache mountPath: /root/.m2 resources: requests: memory: "2Gi" cpu: "1" limits: memory: "4Gi" cpu: "2" restartPolicy: Never volumes: - name: kaniko-secret secret: secretName: docker-registry-secret items: - key: .dockerconfigjson path: config.json - name: maven-cache persistentVolumeClaim: claimName: maven-cache-pvc
4.2 Kubernetes Job for Kaniko Build
# kubernetes/kaniko-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-java-build
namespace: build-system
spec:
template:
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- --dockerfile=Dockerfile
- --context=git://github.com/myorg/my-java-app.git#refs/heads/main
- --destination=docker.io/myorg/my-java-app:${COMMIT_SHA}
- --destination=docker.io/myorg/my-java-app:latest
- --cache=true
- --cache-dir=/cache
- --cache-ttl=168h
- --build-arg=MAVEN_OPTS=-Dmaven.repo.local=/root/.m2/repository
- --label=build.timestamp=$(date +%s)
- --label=commit.sha=${COMMIT_SHA}
- --label=build.env=production
volumeMounts:
- name: docker-config
mountPath: /kaniko/.docker
- name: maven-repo
mountPath: /root/.m2
- name: kaniko-cache
mountPath: /cache
env:
- name: COMMIT_SHA
value: "a1b2c3d4"
resources:
requests:
memory: "4Gi"
cpu: "2"
limits:
memory: "8Gi"
cpu: "4"
volumes:
- name: docker-config
secret:
secretName: docker-registry-secret
items:
- key: .dockerconfigjson
path: config.json
- name: maven-repo
persistentVolumeClaim:
claimName: maven-repo-pvc
- name: kaniko-cache
persistentVolumeClaim:
claimName: kaniko-cache-pvc
restartPolicy: Never
backoffLimit: 1
Part 5: Advanced Kaniko Configurations
5.1 Multi-Architecture Builds for Java
# .codefresh/kaniko-multi-arch.yml
version: '1.0'
steps:
build_amd64:
title: 'Build AMD64 Image'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- /kaniko/executor
--context=.
--dockerfile=Dockerfile
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}-amd64
--platform=linux/amd64
--cache=true
--cache-ttl=72h
build_arm64:
title: 'Build ARM64 Image'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- /kaniko/executor
--context=.
--dockerfile=Dockerfile
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}-arm64
--platform=linux/arm64
--cache=true
--cache-ttl=72h
create_manifest:
title: 'Create Multi-Arch Manifest'
stage: build_container
image: 'docker:latest'
commands:
- |
docker login -u ${{REGISTRY_USERNAME}} -p ${{REGISTRY_PASSWORD}} ${{REGISTRY}}
docker manifest create \
${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}} \
${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}-amd64 \
${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}-arm64
docker manifest push \
${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}
# Tag as latest for main branch
if [ "${{CF_BRANCH}}" = "main" ]; then
docker manifest create \
${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:latest \
${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}-amd64 \
${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}}-arm64
docker manifest push ${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:latest
fi
5.2 Kaniko with Custom Certificates and Proxies
# .codefresh/kaniko-corporate.yml
version: '1.0'
steps:
kaniko_with_corporate_settings:
title: 'Kaniko with Corporate Proxy'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- |
# Copy custom CA certificates
mkdir -p /kaniko/ssl/certs
cp corporate-ca/*.crt /kaniko/ssl/certs/
update-ca-certificates
# Build with proxy settings
/kaniko/executor \
--context=. \
--dockerfile=Dockerfile \
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}} \
--cache=true \
--cache-ttl=24h \
--build-arg=HTTP_PROXY=${{HTTP_PROXY}} \
--build-arg=HTTPS_PROXY=${{HTTPS_PROXY}} \
--build-arg=NO_PROXY=${{NO_PROXY}} \
--registry-mirror=corporate-mirror.example.com \
--skip-tls-verify=false \
--insecure=false
environment:
- HTTP_PROXY=${{CORPORATE_HTTP_PROXY}}
- HTTPS_PROXY=${{CORPORATE_HTTPS_PROXY}}
- NO_PROXY=${{CORPORATE_NO_PROXY}}
Part 6: Security and Best Practices
6.1 Security-Hardened Kaniko Build
# .codefresh/kaniko-secure.yml
version: '1.0'
steps:
secure_kaniko_build:
title: 'Security-Hardened Kaniko Build'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- |
/kaniko/executor \
--context=. \
--dockerfile=Dockerfile.secure \
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}} \
--cache=true \
--cache-ttl=24h \
--ignore-path=/root/.m2/repository \
--ignore-path=/tmp \
--ignore-path=/var/cache \
--label=maintainer="Java Team <[email protected]>" \
--label=org.opencontainers.image.title="My Java Application" \
--label=org.opencontainers.image.description="Spring Boot Java Application" \
--label=org.opencontainers.image.source="https://github.com/myorg/my-java-app" \
--label=org.opencontainers.image.licenses="Apache-2.0" \
--label=org.opencontainers.image.vendor="My Company" \
--no-push=false \
--reproducible=true \
--single-snapshot=true \
--skip-unused-stages=true \
--verbosity=warning
environment:
- DOCKER_CONFIG=/kaniko/.docker
6.2 Security-Hardened Dockerfile
# Dockerfile.secure FROM maven:3.8.6-openjdk-17 AS builder # Security: Run as non-root user RUN groupadd -r maven && useradd -r -g maven maven USER maven WORKDIR /app COPY --chown=maven:maven pom.xml . RUN mvn dependency:go-offline -B COPY --chown=maven:maven src ./src RUN mvn clean package -DskipTests -B # Runtime stage with security hardening FROM eclipse-temurin:17-jre-jammy # Security updates and minimal packages RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y --no-install-recommends \ curl \ ca-certificates && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Create non-privileged user RUN groupadd -r javaapp && \ useradd -r -g javaapp -d /app -s /sbin/nologin -c "Java Application User" javaapp WORKDIR /app USER javaapp COPY --from=builder --chown=javaapp:javaapp /app/target/*.jar app.jar # Security: Read-only root filesystem # Note: This may need adjustment based on application needs # RUN chmod -R a-w /app && chmod a+rx /app/app.jar EXPOSE 8080 # Security: Run with security manager and memory limits ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar"]
Part 7: Monitoring and Optimization
7.1 Build Metrics and Monitoring
# .codefresh/kaniko-metrics.yml
version: '1.0'
steps:
kaniko_build_with_metrics:
title: 'Kaniko Build with Metrics'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- |
START_TIME=$(date +%s)
/kaniko/executor \
--context=. \
--dockerfile=Dockerfile \
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}} \
--cache=true \
--cache-ttl=72h \
--verbosity=info
END_TIME=$(date +%s)
BUILD_DURATION=$((END_TIME - START_TIME))
echo "Build completed in ${BUILD_DURATION} seconds"
# Send metrics to monitoring system
curl -X POST \
-H "Content-Type: application/json" \
-d "{\"build_duration\": ${BUILD_DURATION}, \"image_size\": $(docker inspect ${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}} | jq '.[0].Size'), \"commit_sha\": \"${{CF_SHORT_REVISION}}\", \"branch\": \"${{CF_BRANCH}}\"}" \
${{METRICS_WEBHOOK_URL}}
environment:
- DOCKER_CONFIG=/kaniko/.docker
7.2 Performance Optimization
# .codefresh/kaniko-optimized.yml
version: '1.0'
steps:
optimized_kaniko_build:
title: 'Optimized Kaniko Build for Java'
stage: build_container
image: '${{KANIKO_IMAGE}}'
working_directory: '${{CF_VOLUME_PATH}}/app'
commands:
- |
/kaniko/executor \
--context=. \
--dockerfile=Dockerfile \
--destination=${{REGISTRY}}/${{REPOSITORY}}/${{APP_NAME}}:${{CF_SHORT_REVISION}} \
--cache=true \
--cache-dir=/cache \
--cache-ttl=168h \
--cache-run-layers=true \
--cache-copy-layers=true \
--use-new-run \
--snapshotMode=time \
--compressed-caching=false \
--compression=zstd \
--compression-level=6 \
--image-fs-extract-retry=3 \
--push-retry=3 \
--log-format=color \
--log-timestamp=true
environment:
- DOCKER_CONFIG=/kaniko/.docker
Best Practices for Kaniko with Java
- Layer Caching Strategy:
- Cache Maven dependencies in separate layer
- Use
--cache-run-layersfor RUN command caching - Set appropriate TTL for cache layers
- Security:
- Use non-root users in Dockerfile
- Regularly update base images
- Scan images with Trivy or Grype
- Use content trust when possible
- Performance:
- Use
.dockerignoreto exclude unnecessary files - Leverage multi-stage builds
- Optimize layer ordering
- Use appropriate resource limits
- Reproducibility:
- Pin base image versions
- Use specific Maven/Gradle versions
- Enable
--reproducibleflag
- Monitoring:
- Track build times and cache hit rates
- Monitor image sizes
- Set up alerts for build failures
Conclusion
Kaniko provides a secure, efficient, and Kubernetes-native approach to building Java application containers. By eliminating the need for Docker daemon and running without root privileges, Kaniko addresses critical security concerns in CI/CD pipelines.
For Java teams, Kaniko offers:
- Secure container builds in restricted environments
- Excellent caching for Maven/Gradle dependencies
- Kubernetes-native integration for cluster-based builds
- Multi-architecture support for diverse deployment targets
- Reproducible builds across different environments
By following the patterns and best practices outlined in this guide, Java development teams can implement robust, secure, and efficient container build pipelines using Kaniko, whether in CI/CD systems like Codefresh or directly within Kubernetes clusters.