Spring Boot
Docker
Kubernetes
JVM Arguments
Containerization

Passing JVM args to Docker image of Spring boot app on Kubernetes

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Passing JVM arguments to a Spring Boot app on Kubernetes is easiest when you separate image design from deployment-time tuning. The image should start Java in a predictable way, and Kubernetes should inject the memory flags, GC options, and system properties that belong to a specific environment.

Prefer Environment-Driven JVM Options

A robust pattern is to let the JVM read options from JAVA_TOOL_OPTIONS. This is better than hardcoding production flags into the image because the same image can then run in different environments with different settings.

A simple Dockerfile can stay minimal:

dockerfile
1FROM eclipse-temurin:21-jre
2WORKDIR /app
3COPY app.jar /app/app.jar
4ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Then inject JVM arguments from Kubernetes:

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4  name: myapp
5spec:
6  replicas: 1
7  selector:
8    matchLabels:
9      app: myapp
10  template:
11    metadata:
12      labels:
13        app: myapp
14    spec:
15      containers:
16        - name: myapp
17          image: myrepo/myapp:1.0.0
18          env:
19            - name: JAVA_TOOL_OPTIONS
20              value: "-Xms256m -Xmx512m -Dspring.profiles.active=prod"
21          ports:
22            - containerPort: 8080

The JVM automatically reads JAVA_TOOL_OPTIONS, so you do not need a shell wrapper just to expand the variable.

Use args When You Want Container-Spec Control

Another valid pattern is to place the Java command in the image and override or extend arguments from the pod spec.

dockerfile
ENTRYPOINT ["java"]
CMD ["-jar", "/app/app.jar"]

Then in Kubernetes:

yaml
1containers:
2  - name: myapp
3    image: myrepo/myapp:1.0.0
4    args:
5      - "-Xms256m"
6      - "-Xmx512m"
7      - "-Dspring.profiles.active=prod"
8      - "-jar"
9      - "/app/app.jar"

This gives deployment manifests full control over the final Java invocation. The tradeoff is that the Kubernetes spec must now carry more of the startup command.

Match JVM Memory to Container Limits

Passing -Xmx without thinking about container limits is a common mistake. The JVM heap is only part of total memory usage. Metaspace, thread stacks, and native buffers also consume memory.

A safer rule is to align JVM settings with Kubernetes memory requests and limits instead of choosing heap sizes in isolation.

yaml
1resources:
2  requests:
3    memory: "512Mi"
4    cpu: "250m"
5  limits:
6    memory: "768Mi"
7    cpu: "1"

If the container limit is tight, an oversized heap can cause the pod to be killed even when the heap itself looks reasonable on paper.

Use Spring Properties Only Where They Belong

Some settings should be JVM arguments, and some should not. Examples:

  • '-Xms and -Xmx are JVM flags'
  • '-XX:+UseG1GC is a JVM flag'
  • '-Dspring.profiles.active=prod is a Java system property'
  • 'SPRING_PROFILES_ACTIVE=prod is an application environment variable'

Do not push every application setting into JVM flags just because the Java command happens to be nearby. Spring Boot already reads many settings cleanly from environment variables and config files.

Avoid Image-Specific Magic Variables Unless You Control the Image

Many base images support helper variables such as JAVA_OPTS, but that behavior depends on the image entrypoint. If you do not control the startup script, JAVA_OPTS may do nothing at all.

That is why JAVA_TOOL_OPTIONS is often safer when you want image-agnostic JVM option injection. It is understood by the JVM itself, not by a particular Docker entrypoint convention.

Common Pitfalls

  • Hardcoding environment-specific JVM flags into the Docker image.
  • Setting -Xmx without considering Kubernetes memory limits and non-heap memory.
  • Assuming JAVA_OPTS works in every Java image even when the entrypoint never reads it.
  • Putting the full Java command into a shell wrapper when simpler ENTRYPOINT and environment patterns would be clearer.
  • Mixing JVM flags and ordinary Spring configuration without a clear boundary between them.

Summary

  • Keep the image startup command simple and inject environment-specific JVM flags at deployment time.
  • 'JAVA_TOOL_OPTIONS is a strong default because the JVM reads it directly.'
  • Kubernetes args are another valid option when you want manifest-level control of the command line.
  • Size heap settings with container memory limits in mind.
  • Separate true JVM flags from ordinary Spring Boot configuration.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.