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!
When I first started deploying Java applications, the process was often manual and error-prone. We'd package everything into a massive WAR or JAR file, deploy it to a server, and hope nothing broke. Then Kubernetes came along, and it changed everything. Now, I can manage Java apps in a way that's automated, scalable, and resilient. In this article, I'll share five key techniques I've used to deploy and manage Java applications on Kubernetes, making the process smoother and more reliable.
Let's start with container image optimization. When you're working with Java, the size of your container images can significantly impact deployment speed and security. I remember early on, my images were bloated with build tools and unnecessary dependencies. This made startups slow and increased the risk of vulnerabilities. To fix this, I began using multi-stage Docker builds. This approach separates the build environment from the runtime environment. In the first stage, I use a full JDK image to compile the code and run tests. Then, in the second stage, I copy only the compiled JAR and necessary files into a slim JRE image. This cuts down the image size dramatically and speeds up container startup times.
Here's a practical example of a multi-stage Dockerfile for a Spring Boot application:
# Build stage
FROM maven:3.8.4-openjdk-17 AS build
WORKDIR /workspace/app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
# Runtime stage
FROM openjdk:17-jre-slim
RUN addgroup --system spring && adduser --system spring --ingroup spring
USER spring:spring
WORKDIR /app
COPY --from=build /workspace/app/target/extracted/dependencies/ ./
COPY --from=build /workspace/app/target/extracted/spring-boot-loader/ ./
COPY --from=build /workspace/app/target/extracted/snapshot-dependencies/ ./
COPY --from=build /workspace/app/target/extracted/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
This Dockerfile uses Maven to build the application and then extracts the layers for better caching. The final image is based on openjdk:17-jre-slim, which is much smaller than the full JDK. By doing this, I've reduced image sizes by over 50% in some projects, leading to faster downloads and startups. It's a simple change that makes a big difference in production environments.
Another technique I rely on is deployment strategies for zero-downtime updates. In the past, deploying a new version of an application meant taking the service offline, which wasn't ideal for users. Kubernetes offers rolling updates, which replace pods gradually while ensuring the application remains available. I configure this in the deployment manifest to control how many pods are updated at a time. Health checks are crucial here; they tell Kubernetes when a pod is ready to handle traffic. Without them, you might send requests to pods that are still starting up, causing errors.
Here's a Kubernetes deployment YAML that implements a rolling update with health checks:
apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-service
labels:
app: inventory-service
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2
maxUnavailable: 1
selector:
matchLabels:
app: inventory-service
template:
metadata:
labels:
app: inventory-service
spec:
containers:
- name: inventory-app
image: inventory-service:2.1.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 45
periodSeconds: 15
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 20
failureThreshold: 5
env:
- name: SPRING_PROFILES_ACTIVE
value: "kubernetes"
In this example, the rolling update strategy allows up to two extra pods during the update and ensures at least three pods are always available. The readiness probe checks if the app is ready to serve requests, and the liveness probe restarts the pod if it becomes unresponsive. I've used this in production to update applications without users noticing any interruption. It's like changing the tires on a moving car—smooth and seamless.
Configuration externalization is another game-changer. I used to hardcode configuration values into my Java apps, which meant rebuilding images for different environments. This was tedious and error-prone. Now, I use Kubernetes ConfigMaps and Secrets to manage configuration outside the application code. ConfigMaps hold non-sensitive data, like environment variables or configuration files, while Secrets handle sensitive information like passwords and API keys. This separation makes it easy to promote the same image from development to production without changes.
Here's how I define a ConfigMap and Secret for a Java application:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
application.properties: |
server.port=8080
spring.datasource.url=jdbc:postgresql://postgres-service:5432/mydb
logging.level.com.example=INFO
app.feature.flags=enabled
---
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
db-password: cGFzc3dvcmQxMjM= # base64 encoded 'password123'
api-key: YXBpLWtleS1zZWNyZXQ= # base64 encoded 'api-key-secret'
Then, I mount these into the deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-java-app
spec:
template:
spec:
containers:
- name: app
image: my-java-app:latest
volumeMounts:
- name: config-volume
mountPath: /app/config
- name: secret-volume
mountPath: /app/secrets
readOnly: true
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
volumes:
- name: config-volume
configMap:
name: app-config
- name: secret-volume
secret:
secretName: app-secrets
In my Java code, I can read these configurations using Spring Boot's @Value annotation or by loading properties files. For example:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class DatabaseConfig {
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${db-password}")
private String dbPassword;
// Use these in your data source configuration
}
This approach has saved me countless hours. I can update configurations on the fly without redeploying the entire application. It also improves security by keeping secrets out of the image and version control.
Service discovery is essential for microservices architectures. In a Kubernetes cluster, pods are ephemeral—they can come and go as scaling or failures occur. Hardcoding IP addresses in your Java apps isn't practical. Instead, I use Kubernetes Services to provide stable endpoints for pods. Services act as load balancers, routing traffic to healthy pods based on labels. For external access, I configure Ingress resources to handle routing from outside the cluster.
Here's a simple Service and Ingress setup:
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: main-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: api.mycompany.com
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
In my Java application, I can use service names directly for communication. For instance, if I have a order-service that needs to call the user-service, I can use the URL http://user-service:80. Kubernetes handles the DNS resolution internally. Here's a snippet from a Spring Boot RestTemplate call:
import org.springframework.web.client.RestTemplate;
import org.springframework.stereotype.Service;
@Service
public class UserServiceClient {
private final RestTemplate restTemplate;
public UserServiceClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public User getUserById(Long id) {
String url = "http://user-service/users/" + id;
return restTemplate.getForObject(url, User.class);
}
}
I remember when I first set this up; it felt like magic. No more worrying about IP changes or manual load balancer configurations. Services and Ingress make inter-service communication reliable and straightforward.
Resource management is the fifth technique I want to highlight. In a shared cluster, it's easy for one application to hog all the resources, causing others to suffer. By setting CPU and memory requests and limits, I ensure fair resource allocation. Requests are what the container is guaranteed to get, and limits are the maximum it can use. This prevents resource exhaustion and helps the scheduler place pods efficiently. Additionally, Horizontal Pod Autoscaling (HPA) automatically adjusts the number of pods based on CPU or memory usage.
Here's a deployment with resource limits and an HPA configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
template:
spec:
containers:
- name: payment-app
image: payment-service:1.0.0
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: payment-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: payment-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 75
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
In this example, the payment-service requests 100 milliCPU units and 256Mi of memory, with limits set to 500m CPU and 512Mi memory. The HPA will scale the pods between 2 and 10 based on CPU and memory usage. I've seen this in action during traffic spikes; the system scales up automatically and scales down when load decreases, saving costs and maintaining performance.
To make this work well with Java, I often tune the JVM settings based on the container limits. For instance, I set the heap size to stay within the memory limits to avoid the JVM being killed by Kubernetes. Here's a sample Dockerfile entry that sets JVM options:
FROM openjdk:17-jre-slim
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC"
WORKDIR /app
COPY app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
And in the Kubernetes deployment, I might pass environment variables for further customization:
env:
- name: JAVA_TOOL_OPTIONS
value: "-Xms256m -Xmx512m"
This ensures the JVM respects the container's resource boundaries. I learned this the hard way when an app kept getting killed due to memory issues; now, I always set these options.
Putting it all together, these techniques have transformed how I deploy Java applications. They make the process more reliable, scalable, and easier to manage. I encourage you to start small—perhaps with image optimization or configuration externalization—and gradually incorporate the others. The investment in learning Kubernetes pays off in reduced operational overhead and happier users.
If you're new to this, don't be intimidated. The Kubernetes community is vast, and there are plenty of resources to help. I often refer to the official documentation and experiment in a local setup using tools like Minikube or Kind. Practice with sample applications, and soon, you'll be deploying Java apps on Kubernetes like a pro.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
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 | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS 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)