DEV Community

Cover image for Master Java Containerization: Essential Tools for Scalable Deployments
Aarav Joshi
Aarav Joshi

Posted on

Master Java Containerization: Essential Tools for Scalable Deployments

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Java containers and orchestration have transformed how we build, deploy, and manage applications. As a Java developer with experience in containerization, I've seen firsthand how these technologies solve complex deployment challenges while enabling scalability. This article explores the essential tools that make Java containerization effective and manageable at scale.

Docker for Java Applications

Docker provides a standardized way to package Java applications with their dependencies. For Java developers, this means consistent environments across development, testing, and production.

When containerizing Java applications, the JVM's memory management presents unique challenges. By default, Java containers don't respect memory limits set by Docker. For Java 8u131+ and Java 10+, the JVM is container-aware, but explicit configuration is still recommended:

FROM openjdk:17-slim
COPY target/myapp.jar app.jar
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "/app.jar"]
Enter fullscreen mode Exit fullscreen mode

Multi-stage builds significantly reduce image size, which is crucial for Java applications:

FROM maven:3.8.6-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Enter fullscreen mode Exit fullscreen mode

This approach separates the build environment from the runtime environment, resulting in smaller images and faster deployments.

For Spring Boot applications, the Spring Boot Maven/Gradle plugins can create optimized Docker images:

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp
Enter fullscreen mode Exit fullscreen mode

Kubernetes for Java Application Orchestration

Kubernetes excels at orchestrating containerized Java applications, especially in microservices architectures. Its declarative approach to infrastructure management provides predictability and stability.

A basic Kubernetes deployment for a Java application looks like this:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: java-app
  template:
    metadata:
      labels:
        app: java-app
    spec:
      containers:
      - name: java-app
        image: myorg/java-app:1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1"
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 60
          periodSeconds: 15
Enter fullscreen mode Exit fullscreen mode

This configuration includes important elements for Java applications:

  • Resource requests and limits to manage JVM memory usage
  • Health probes to ensure the application is properly initialized (especially important for Java applications with longer startup times)
  • Multiple replicas for scalability

For stateful Java applications like databases, StatefulSets provide stable network identities and persistent storage:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: java-database
spec:
  serviceName: "database"
  replicas: 3
  selector:
    matchLabels:
      app: database
  template:
    metadata:
      labels:
        app: database
    spec:
      containers:
      - name: database
        image: myorg/java-database:1.0
        volumeMounts:
        - name: data
          mountPath: /var/lib/database
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi
Enter fullscreen mode Exit fullscreen mode

Spring Cloud Kubernetes

Spring Cloud Kubernetes bridges the gap between Spring Boot applications and Kubernetes. It allows Spring applications to leverage Kubernetes native features while maintaining Spring's programming model.

To use Spring Cloud Kubernetes, add the dependency to your project:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-client-all</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

With this integration, your Spring application can:

  1. Discover other services through Kubernetes Service Discovery:
@Service
public class ProductService {
    @Autowired
    private DiscoveryClient discoveryClient;

    public List<String> getServices() {
        return discoveryClient.getServices();
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Load configuration from Kubernetes ConfigMaps and Secrets:
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {
    private String property;

    // getters and setters
}
Enter fullscreen mode Exit fullscreen mode

With the corresponding ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  application.properties: |
    app.property=value
Enter fullscreen mode Exit fullscreen mode

Spring Cloud Kubernetes also supports features like distributed tracing and circuit breaking, making it ideal for microservices architecture.

JIB: Containerizing Java Applications Without Docker

JIB simplifies container image creation for Java applications. It builds optimized Docker images directly from Maven or Gradle without requiring a Docker daemon.

To use JIB with Maven:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>3.3.1</version>
    <configuration>
        <to>
            <image>myregistry/myapp:${project.version}</image>
        </to>
        <container>
            <jvmFlags>
                <jvmFlag>-Xms512m</jvmFlag>
                <jvmFlag>-Xmx1g</jvmFlag>
            </jvmFlags>
            <ports>
                <port>8080</port>
            </ports>
        </container>
    </configuration>
</plugin>
Enter fullscreen mode Exit fullscreen mode

JIB's key benefits for Java applications include:

  1. Layer optimization - JIB intelligently separates dependencies from application code, improving rebuild times
  2. Reproducible builds - Images are built in a consistent environment
  3. CI/CD friendly - No need for Docker daemon in build pipelines

To build the image with JIB:

mvn compile jib:build
Enter fullscreen mode Exit fullscreen mode

JIB also integrates with Gradle:

plugins {
    id 'com.google.cloud.tools.jib' version '3.3.1'
}

jib {
    to {
        image = 'myregistry/myapp:1.0'
    }
    container {
        jvmFlags = ['-Xms512m', '-Xmx1g']
        ports = ['8080']
    }
}
Enter fullscreen mode Exit fullscreen mode

Helm for Managing Java Application Deployments

Helm streamlines the deployment and management of Java applications on Kubernetes through charts and templates. It's particularly valuable for complex Java deployments with multiple services.

A basic Helm chart for a Java application includes:

# Chart.yaml
apiVersion: v2
name: java-app
description: A Java application Helm chart
type: application
version: 0.1.0
appVersion: 1.0.0
Enter fullscreen mode Exit fullscreen mode
# values.yaml
replicaCount: 2

image:
  repository: myorg/java-app
  tag: 1.0.0
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

resources:
  requests:
    memory: 512Mi
    cpu: 500m
  limits:
    memory: 1Gi
    cpu: 1

javaOpts: "-Xms256m -Xmx512m"
Enter fullscreen mode Exit fullscreen mode
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "java-app.fullname" . }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "java-app.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "java-app.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          env:
            - name: JAVA_OPTS
              value: {{ .Values.javaOpts }}
          ports:
            - containerPort: 8080
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
Enter fullscreen mode Exit fullscreen mode

To deploy the application:

helm install my-java-app ./java-app
Enter fullscreen mode Exit fullscreen mode

Helm's templating allows for customization across environments, making it easy to manage different configurations for development, staging, and production. For Java applications, this means configuring environment-specific JVM parameters, connection pools, and feature flags.

Creating a Complete Deployment Pipeline

A complete deployment pipeline for Java applications combines these tools to achieve continuous delivery:

  1. Build the Java application with Maven/Gradle
  2. Create a container image with JIB or Docker
  3. Push the image to a registry
  4. Deploy to Kubernetes using Helm

Here's a GitHub Actions workflow example:

name: Build and Deploy Java App

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up JDK
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'

    - name: Build with Maven
      run: mvn clean package

    - name: Build and Push Image with JIB
      run: |
        mvn compile jib:build \
          -Djib.to.auth.username=${{ secrets.REGISTRY_USERNAME }} \
          -Djib.to.auth.password=${{ secrets.REGISTRY_PASSWORD }}

    - name: Set up kubectl
      uses: azure/setup-kubectl@v3

    - name: Set up Helm
      uses: azure/setup-helm@v3

    - name: Configure Kubernetes
      run: |
        echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig
        export KUBECONFIG=./kubeconfig

    - name: Deploy to Kubernetes with Helm
      run: |
        helm upgrade --install my-java-app ./charts/java-app \
          --set image.tag=${GITHUB_SHA::7} \
          --namespace production
Enter fullscreen mode Exit fullscreen mode

Performance Considerations for Java in Containers

Java applications in containers require special attention to memory management. The JVM garbage collector behavior can impact container performance.

For Java applications in containers:

  1. Use G1GC for modern applications:
FROM openjdk:17-slim
COPY target/app.jar app.jar
ENTRYPOINT ["java", "-XX:+UseG1GC", "-jar", "/app.jar"]
Enter fullscreen mode Exit fullscreen mode
  1. Set memory limits appropriately:
resources:
  requests:
    memory: "768Mi"
  limits:
    memory: "1Gi"
Enter fullscreen mode Exit fullscreen mode
  1. Configure JVM based on container memory:
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "/app.jar"]
Enter fullscreen mode Exit fullscreen mode
  1. Optimize for startup time with Application CDS:
FROM openjdk:17-slim as cds
COPY target/app.jar app.jar
RUN java -Xshare:dump -XX:SharedArchiveFile=app.jsa -jar app.jar

FROM openjdk:17-slim
COPY --from=cds /app.jsa /app.jsa
COPY --from=cds /app.jar /app.jar
ENTRYPOINT ["java", "-XX:SharedArchiveFile=app.jsa", "-Xshare:auto", "-jar", "/app.jar"]
Enter fullscreen mode Exit fullscreen mode

Monitoring Java Applications in Kubernetes

Effective monitoring is crucial for containerized Java applications. Prometheus and Grafana provide powerful monitoring capabilities:

  1. Add Micrometer to your Spring Boot application:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
  1. Configure application.properties:
management.endpoints.web.exposure.include=prometheus,health,info
management.endpoint.health.show-details=always
Enter fullscreen mode Exit fullscreen mode
  1. Create a ServiceMonitor for Prometheus:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: java-app-monitor
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: java-app
  endpoints:
  - port: http
    path: /actuator/prometheus
    interval: 15s
Enter fullscreen mode Exit fullscreen mode
  1. Set up JVM dashboards in Grafana to monitor:
    • Heap usage
    • Garbage collection metrics
    • Thread counts
    • Response times

Scaling Considerations

Horizontal Pod Autoscaler (HPA) can automatically scale Java applications based on CPU or memory usage:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: java-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: java-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
Enter fullscreen mode Exit fullscreen mode

For Java applications with slower startup times, consider:

  1. Implementing readiness probes with appropriate delays
  2. Using preStop hooks to allow graceful shutdown
  3. Configuring proper terminationGracePeriodSeconds
spec:
  containers:
  - name: java-app
    # ...
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 10"]
    readinessProbe:
      httpGet:
        path: /actuator/health/readiness
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 5
  terminationGracePeriodSeconds: 60
Enter fullscreen mode Exit fullscreen mode

Security Best Practices

Security is paramount for containerized Java applications:

  1. Run containers as non-root users:
FROM openjdk:17-jre-slim
RUN groupadd -r javauser && useradd -r -g javauser javauser
USER javauser
COPY target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
Enter fullscreen mode Exit fullscreen mode
  1. Scan images for vulnerabilities:
- name: Scan container image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myorg/java-app:latest'
    format: 'table'
    exit-code: '1'
    severity: 'CRITICAL'
Enter fullscreen mode Exit fullscreen mode
  1. Use network policies to restrict traffic:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: java-app-network-policy
spec:
  podSelector:
    matchLabels:
      app: java-app
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
Enter fullscreen mode Exit fullscreen mode

Conclusion

Containerization and orchestration have fundamentally changed how we deploy Java applications. The combination of Docker, Kubernetes, Spring Cloud Kubernetes, JIB, and Helm creates a powerful toolkit for building scalable, resilient Java deployments.

I've found that these technologies not only simplify deployment but also improve development workflows through consistency and automation. The modularity of containerized applications aligns perfectly with microservices architectures, enabling teams to develop, deploy, and scale components independently.

As Java developers, our focus should remain on writing quality code while leveraging these tools to handle the complexity of modern deployment environments. With the patterns and practices outlined in this article, you can create robust deployment pipelines that grow with your application needs.

The container ecosystem continues to evolve rapidly, but the core principles of immutability, declarative configuration, and automation remain constant. By building on these foundations, you can create Java deployments that are both reliable and scalable, regardless of how your infrastructure needs change over time.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)