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 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: 404My 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:
management: endpoints: web: exposure: include: healthBut when I checked the available endpoints:
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:
management: endpoints: web: exposure: include: health endpoint: health: probes: enabled: trueThen I tested the endpoints:
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:
livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10
readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 5The correct paths should be /actuator/health/liveness and /actuator/health/readiness:
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: 3After applying the corrected configuration:
kubectl apply -f k8s/deployment.yamlkubectl get podsNAME READY STATUS RESTARTS AGEmyapp-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 myappThe 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:
-
Liveness Probe: Tells Kubernetes whether the application is alive. If it fails, Kubernetes restarts the pod. Use this for detecting deadlocks or unrecoverable states.
-
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.
-
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:
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: 30sFor applications with slow startup times, I added a startup probe:
startupProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 0 periodSeconds: 10 failureThreshold: 30This 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:
import org.springframework.boot.actuate.health.Health;import org.springframework.boot.actuate.health.HealthIndicator;import org.springframework.stereotype.Component;
@Componentpublic 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:
-
Setting initialDelaySeconds too low: My app takes 45 seconds to start, but I set
initialDelaySeconds: 10for the liveness probe. The probe kept failing during startup. -
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.
-
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.
-
Setting failureThreshold too aggressive: I used
failureThreshold: 1initially, which caused pods to restart on any transient network issue. UsingfailureThreshold: 3gives more tolerance.
Summary
In this post, I showed how to configure Kubernetes probes with Spring Boot Actuator. The key points are:
- Enable probe endpoints with
management.endpoint.health.probes.enabled: true - Use
/actuator/health/livenessfor liveness probe - Use
/actuator/health/readinessfor readiness probe - Configure appropriate delays based on your application’s startup time
- Use startup probes for slow-initializing applications
- 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:
- 👨💻 Spring Boot Actuator Health Endpoints
- 👨💻 Kubernetes Configure Liveness, Readiness and Startup Probes
- 👨💻 MicroProfile Health Specification
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments