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.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.