Skip to content

If you worked with MicroProfile Health, you already know Kubernetes probes in Spring Boot

Purpose

This post shows how to configure Kubernetes probes with Spring Boot Actuator for proper pod lifecycle management.

When I deployed my Spring Boot application to Kubernetes, I got this problem:

kubectl describe pod output
kubectl describe pod myapp-6d4b8c9f-x2kp7
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 12s (x3 over 32s) kubelet Liveness probe failed: HTTP probe failed with statuscode: 404
Warning Unhealthy 8s (x2 over 18s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 404

My pods kept restarting because Kubernetes couldn’t find the health endpoints.

Environment

  • Spring Boot 3.2.1
  • Java 21
  • Kubernetes 1.28
  • Spring Boot Actuator

What happened?

I thought I had configured everything correctly. My application.yml had the Actuator endpoint exposed:

application.yml
management:
endpoints:
web:
exposure:
include: health

But when I checked the available endpoints:

curl commands
curl http://localhost:8080/actuator
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
}
}
}

The health endpoint was there, but I needed /actuator/health/liveness and /actuator/health/readiness for Kubernetes probes. Those endpoints were missing.

How to solve it?

I tried to enable the probes endpoints explicitly:

application.yml
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
probes:
enabled: true

Then I tested the endpoints:

curl commands
curl http://localhost:8080/actuator/health/liveness
{
"status": "UP"
}
curl http://localhost:8080/actuator/health/readiness
{
"status": "UP"
}

Now the endpoints were available. But there was another issue - my Kubernetes probes were still failing because I was checking the wrong path.

I had configured my deployment like this:

deployment.yaml
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5

The correct paths should be /actuator/health/liveness and /actuator/health/readiness:

deployment.yaml
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3

After applying the corrected configuration:

curl commands
kubectl apply -f k8s/deployment.yaml
kubectl get pods
NAME READY STATUS RESTARTS AGE
myapp-6d4b8c9f-x2kp7 1/1 Running 0 2m
kubectl describe pod myapp-6d4b8c9f-x2kp7
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulled 2m kubelet Successfully pulled image
Normal Created 2m kubelet Created container myapp
Normal Started 2m kubelet Started container myapp

The pods were now running without restarts.

The reason

I think the key reason for the confusion is that Spring Boot Actuator’s health probes follow the same concepts as MicroProfile Health:

  1. Liveness Probe: Tells Kubernetes whether the application is alive. If it fails, Kubernetes restarts the pod. Use this for detecting deadlocks or unrecoverable states.

  2. Readiness Probe: Tells Kubernetes whether the application is ready to accept traffic. If it fails, Kubernetes stops routing traffic to the pod but doesn’t restart it.

  3. Startup Probe: For slow-starting applications, this gives them time to initialize before liveness checks begin.

The HTTP status codes follow the same convention as MicroProfile:

  • HTTP 200 = UP (healthy)
  • HTTP 503 = DOWN (unhealthy)

I also realized I needed to handle graceful shutdown properly. Spring Boot provides this out of the box:

application.yml
server:
shutdown: graceful
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
probes:
enabled: true
show-details: never
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
spring:
lifecycle:
timeout-per-shutdown-phase: 30s

For applications with slow startup times, I added a startup probe:

deployment.yaml
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 30

This gives the application up to 5 minutes (30 x 10 seconds) to start before Kubernetes considers it failed.

Custom Health Indicators

I also needed to add custom health checks for my database connectivity:

DatabaseReadinessIndicator.java
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class DatabaseReadinessIndicator implements HealthIndicator {
private final DataSource dataSource;
public DatabaseReadinessIndicator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
if (conn.isValid(2)) {
return Health.up().build();
}
return Health.down().withDetail("error", "Connection not valid").build();
} catch (SQLException e) {
return Health.down(e).build();
}
}
}

This affects the readiness probe - when the database is unavailable, the pod won’t receive traffic.

Common Mistakes

I made a few mistakes along the way:

  1. Setting initialDelaySeconds too low: My app takes 45 seconds to start, but I set initialDelaySeconds: 10 for the liveness probe. The probe kept failing during startup.

  2. Using liveness for dependency checks: I initially put database health checks in the liveness probe. This caused unnecessary restarts when the database was temporarily unavailable. Dependency checks should affect readiness, not liveness.

  3. Not configuring startup probe for slow apps: For applications that need more than 60 seconds to initialize, a startup probe is essential to prevent premature restarts.

  4. Setting failureThreshold too aggressive: I used failureThreshold: 1 initially, which caused pods to restart on any transient network issue. Using failureThreshold: 3 gives more tolerance.

Summary

In this post, I showed how to configure Kubernetes probes with Spring Boot Actuator. The key points are:

  1. Enable probe endpoints with management.endpoint.health.probes.enabled: true
  2. Use /actuator/health/liveness for liveness probe
  3. Use /actuator/health/readiness for readiness probe
  4. Configure appropriate delays based on your application’s startup time
  5. Use startup probes for slow-initializing applications
  6. Put dependency health checks in readiness, not liveness

If you worked with MicroProfile Health before, this will feel familiar - the concepts and HTTP status codes are the same.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments