Building Containers Securely: A Complete Guide to Kaniko for Java Applications

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:

  1. Dependency Resolution: Maven/Gradle dependencies are cached efficiently
  2. Multi-Stage Builds: Perfect for separating build and runtime environments
  3. JAR Optimization: Optimized layer caching for application JARs
  4. 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

  1. Layer Caching Strategy:
  • Cache Maven dependencies in separate layer
  • Use --cache-run-layers for RUN command caching
  • Set appropriate TTL for cache layers
  1. Security:
  • Use non-root users in Dockerfile
  • Regularly update base images
  • Scan images with Trivy or Grype
  • Use content trust when possible
  1. Performance:
  • Use .dockerignore to exclude unnecessary files
  • Leverage multi-stage builds
  • Optimize layer ordering
  • Use appropriate resource limits
  1. Reproducibility:
  • Pin base image versions
  • Use specific Maven/Gradle versions
  • Enable --reproducible flag
  1. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper