Spring Boot Actuator, Health Checks, and Not Leaking Your Internals
Actuator is one dependency and gives you health, metrics, and a good deal of introspection. It is also the most common way a Spring Boot application ends up publishing its configuration, including secrets, to anyone who can reach it. Both halves are worth getting right.
The default posture
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Out of the box only /actuator/health is exposed over HTTP, and it reports UP with no detail. That default is deliberately conservative and it is a good starting point. The dangerous step is the one people take next:
management.endpoints.web.exposure.include=*
That exposes /actuator/env, which lists every property the application resolved, and /actuator/configprops, which does the same for bound configuration objects. Spring masks values whose key matches a pattern like password, secret, or key, but a property named app.stripe.token or datasource.jdbc-url with credentials embedded in it sails straight through. It also exposes /actuator/heapdump, which is a complete download of your process memory.
Expose what you need, by name:
management.endpoints.web.exposure.include=health,info,prometheus
And move the whole thing to a separate port that your ingress does not route publicly:
management.server.port=9090
A separate port is a stronger boundary than a path rule, because it cannot be defeated by a misconfigured route or a path-normalisation bug. If actuator must share the main port, put it behind authentication with Spring Security and a distinct role.
Health details
management.endpoint.health.show-details=when-authorized
The options are never, when-authorized, and always. always on a public endpoint tells anyone who asks which database you use, which message broker, and which of them is currently broken. That is a reconnaissance gift. when-authorized gives operators the detail and everyone else a bare status.
Liveness and readiness are not the same thing
Kubernetes asks two different questions, and answering both with the same endpoint causes outages.
Liveness means "is this process beyond saving". A failure restarts the pod. Readiness means "should traffic be sent here right now". A failure removes the pod from the load balancer and leaves it running.
Spring Boot models both:
management.endpoint.health.probes.enabled=true
That gives /actuator/health/liveness and /actuator/health/readiness. Point the probes at them separately.
The failure this prevents is the cascade. If liveness is wired to a health check that includes the database, and the database has a brief hiccup, every pod fails liveness simultaneously and the orchestrator restarts your entire fleet. The database is now facing a thundering herd of cold applications reconnecting, which extends the outage it was recovering from. Liveness should check almost nothing: the process is running and the event loop is not wedged. Dependencies belong in readiness.
Custom indicators
@Component
public class PaymentGatewayHealth implements HealthIndicator {
@Override
public Health health() {
try {
gateway.ping();
return Health.up().build();
} catch (Exception e) {
return Health.down().withDetail("error", e.getMessage()).build();
}
}
}
Any bean implementing HealthIndicator is picked up automatically and its status rolls into the aggregate. That aggregation is the trap: a DOWN from any indicator makes the whole endpoint DOWN, which makes readiness fail, which takes the instance out of rotation. So this indicator declares that your service is unusable whenever a third-party payment gateway is slow, even if nine tenths of your endpoints do not touch payments.
Two rules. Only report DOWN for dependencies your service genuinely cannot function without. And give the check a timeout shorter than the probe timeout, or a slow dependency turns into a hanging health check and the orchestrator reads the timeout as a failure anyway.
For everything else, report the state as a metric and alert on it, rather than folding it into readiness.
Metrics
/actuator/prometheus appears once Micrometer's registry is on the classpath, and it exposes JVM, HTTP, and connection pool metrics with no code. The pool metrics are the ones to watch first, because hikaricp.connections.pending climbing is the early signal for most of the transaction and connection problems that otherwise surface as opaque timeouts.
Keep tag cardinality low. A tag whose value is a user id or a raw URL path creates a new time series per value and will eventually take down your metrics backend rather than your application.
A workable baseline
Separate management port, expose health, info, and prometheus only, details when authorized, probes enabled with liveness kept trivially simple, and custom indicators limited to hard dependencies with short timeouts. That gives operators what they need and gives an attacker nothing.