1. Logs vs. Actuator
If logs are the application's diary (sometimes honest, sometimes “oops, I forgot to write that down”), then Actuator is more like a medical chart: blood pressure, pulse, temperature, and other “right now” indicators. And in a containerized environment, such “health indicators” are especially valuable, because we want to quickly distinguish “the process is alive, but the service is not ready” from “everything really works”.
Logs are great at answering the question “what happened earlier”. For example, we can see that the application started, read the config, and brought up the web server. But there are scenarios where history does not help much. The service may be running, but hung; it may return 500s; it may be overloaded; or it may simply be listening on the wrong port. And at those moments, you want a simple technical URL that you can open and get a short answer without guessing from the stack trace.
Actuator is exactly about this kind of technical observability contract: it adds separate endpoints to the application, unrelated to the business API (/api/catalog/...), and exposes “operational truth” through them. For this course, we intentionally limit ourselves to the minimal set: /actuator/health, /actuator/info, /actuator/metrics. That is enough to make the service “visible” and manageable without turning the course into a separate observability track.
A small table to pin down the roles:
| Tool | What it gives you | When it is useful |
|---|---|---|
| docker logs | Event history | “Why did it not start?”, “What did we do before the crash?” |
|
A snapshot of “alive / not alive” | “Has the service come up?”, “Can we already call it?” |
|
Service passport | “What version is it?”, “What mode is it in?” |
|
A list of metrics and values | “How many requests?”, “How is memory doing?”, “Are there any metrics at all?” |
2. Actuator endpoints
Actuator in Spring Boot is not “magic in the air”, but the ordinary spring-boot-starter-actuator starter, which adds a set of ready-made endpoints to your application. These endpoints are technical, meaning they are intended not for your business API clients, but for you (as a developer) and for the containerized environment that wants to understand the service state.
It is important to catch one psychological point. Beginners often perceive Actuator as “another bunch of URLs to memorize”. In reality, the value is not in the URLs, but in the model: Actuator creates an operational layer separated from the business layer. It is like a separate entrance to a building for maintenance staff: visitors do not use it, but without it the building turns into a black box.
In our Container-Ready Catalog Service, Actuator is already there by baseline (we already have /actuator/health working). That means now we are not “adding a library”, but configuring the minimal operational contract and learning how to use it. Another important nuance: by default, Actuator is not required to expose all endpoints externally. And that is good. An overly exposed Actuator is like “an admin panel without a password”: one day you will forget that it is even hanging out there in the world.
So in this lecture, we keep two rules in mind at all times. First — expose exactly what is needed. Second — do not put sensitive data there. We are not discussing security as a separate course yet (Spring Security will live in another universe), but we are already building the habit of “not leaking extra stuff”.
3. /actuator/health: liveness check
/actuator/health is the endpoint that should answer the most basic question: “is the application okay or not?” And that is exactly why it is almost always the first thing checked when a service starts in a container. In the ideal world, you want to make one HTTP request and get a very short, clear answer without reading 500 lines of logs.
In a typical case, the response looks minimalist. Example (illustrative, but close to reality):
{
// Overall application status used by both people and container automation
"status": "UP"
}
The meaning here is exactly this: “I am alive, the controllers are up, the Spring context has been created, and the application generally feels fine.” At the HTTP-status level, the common mapping is straightforward: when the status is UP, the endpoint returns 200 OK. When the status is not UP, it usually returns an error-range code, often 503 Service Unavailable. That is convenient: clear for humans, easy for automation.
Now for an important point that often breaks expectations. health is not a business-logic check. It is not “can we create a CatalogItem and export CSV”. It is a basic operational signal. It should be fast and stable. If you make health heavy, it will turn from a “thermometer” into a “washing machine that also measures temperature, but somehow takes half an hour”.
Another thing: inside health, there are usually so-called “health indicators”. For example, if in postgres mode the application connects to a database, then health may take datasource availability into account. In standalone mode, the database is not needed, and health will be simpler. For the current service, built-in indicators are enough; custom health indicators here would only add another axis of complexity.
Sometimes health responses include details (components). In dev mode this is useful, but in general scenarios you need to remember security and noise. In the course's container logic, we only need UP/DOWN at minimum.
Example request from the host (when the container has forwarded the port):
# Check that the application has come up and Actuator responds
curl -s http://localhost:8080/actuator/health
If you see {"status":"UP"}, you can consider the service to be alive at least in the basic sense. And if not, that is a signal to go back to the logs and configuration.
4. /actuator/info: application passport
If health answers “am I alive?”, then info answers “who am I, anyway?” In ordinary development, that seems like a pleasant extra, until you run into reality: you have multiple run modes, different profiles, different images, different parameters. And you want to quickly understand what exactly is running right now.
This is where info unexpectedly becomes super useful. It lets you expose a small JSON with safe information: service name, a nominal version, “mode” (standalone/postgres), and maybe build description. The word “safe” is the key here: info is not the place for passwords, tokens, or internal URLs with secrets (even if it is very tempting “for convenience”).
The simplest way to populate /actuator/info is with info.* properties in application.yml. For example:
info:
# Any data here must be safe to publish externally
app:
name: "catalog-service"
description: "Container-Ready Catalog Service"
# Reflect the run profile: useful for checking which mode the container was started in
mode: "${SPRING_PROFILES_ACTIVE:standalone}"
There is a small trick here: we substitute mode from the env var SPRING_PROFILES_ACTIVE, and if it is missing, we assume standalone mode. This is not Docker magic; it is ordinary Spring property substitution. As a result, inside the container you can change the profile, and /actuator/info will honestly reflect the new mode.
An example response might look like this:
{
"app": {
// Service name: helps you quickly make sure you are looking at the “right” container
"name": "catalog-service",
"description": "Container-Ready Catalog Service",
// Current run mode/profile (for example, standalone or postgres)
"mode": "standalone"
}
}
Once again: this is not “API documentation”, not “database state”, and not “the entire application configuration”. It is a short card that helps you quickly verify that the exact service and the exact mode you expected are running.
5. /actuator/metrics: the service's “fitness tracker”
If health is “alive/dead”, and info is a “passport”, then metrics is a fitness tracker: steps, pulse, sleep (and yes, sometimes it lies too, if you forgot to wear it). In Spring Boot, metrics are usually collected through Micrometer, and Actuator provides HTTP access to them.
It is important to understand that /actuator/metrics is an endpoint “for an engineer's eyes”, not something for a human to read from start to finish. Its typical use case is to check two things: which metrics are available at all, and how to retrieve the value of a specific metric.
The first entry point is most often just the list of names:
# Get a list of available metrics (names only, without values)
curl -s http://localhost:8080/actuator/metrics
The response looks roughly like this:
{
// List of metric names that can be queried individually
"names": [
"jvm.memory.used",
"jvm.threads.live",
"http.server.requests"
]
}
There can be a lot of names there — that is normal. We are not trying to “read everything”. We act like engineers: choose the one we care about and ask about it directly.
For example, a memory metric:
curl -s http://localhost:8080/actuator/metrics/jvm.memory.used
The response usually contains measurements and possible tags (for example, by memory area type). A rough shape:
{
"name": "jvm.memory.used",
"measurements": [
// Primary measurement: the current metric value
{ "statistic": "VALUE", "value": 34567890 }
],
"availableTags": [
// Hints about which dimensions can be used to “slice” the metric (heap/nonheap, etc.)
{ "tag": "area", "values": ["heap", "nonheap"] }
]
}
Likewise, a very practical metric for a web service is http.server.requests. It lets you see that the application is actually receiving requests, and it often supports tags such as status, method, and uri. Example request:
curl -s http://localhost:8080/actuator/metrics/http.server.requests
An important “school” idea: metrics are not a replacement for logs. Metrics do not answer the question “why exactly did it fail”, but rather “how the system behaves in numbers”. If your request count suddenly grows and 500s start appearing, metrics and logs together give you a full picture. Separately, they only give half the truth.
And one more nuance: do not expect /metrics to “explain” everything to you. It gives data, not interpretation. You do the interpretation. It is like a thermometer: it does not say “drink tea”; it says “38.7”.
6. Endpoint exposure
The most common situation for a beginner looks like this: “I know /actuator/info exists... but it is somehow 404.” And that is not a bug, but intentional security: Actuator endpoints do not have to be available over HTTP all at once. Even if they “exist inside the application”, they may not be exposed externally.
For our course, we are fixing a simple rule: we need health, info, and metrics, and we expose exactly those. This is done with the management.endpoints.web.exposure.include setting.
Minimal example in application.yml:
management:
endpoints:
web:
exposure:
include: "health,info,metrics"
This is literally the list of “what can be shown externally over HTTP”. You can think of Actuator endpoints as rooms in a building, and exposure.include as the list of keys you hand out. No key issued — the room exists, but the door is locked.
In a containerized environment, we have a nice bonus: this list can be set via an env var without rebuilding the image. For example:
# The same thing, but through an environment variable (convenient for Docker/Kubernetes)
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,metrics
The point of this, in our general idea of the day, is: the same image, different run modes. And here is the example: you do not rewrite the Dockerfile to “enable metrics”, you change the run configuration.
Again: we do not expose everything indiscriminately. Actuator has many endpoints, and some of them may be sensitive. If you expose *, you are almost guaranteed to eventually get the situation “oops, we exposed something we did not plan to.” In the training project, we need a minimal set, and that will become a habit.
7. Example configuration and requests
In practice, everything comes down to very down-to-earth things: we want our service to reliably respond to three URLs and do so consistently in a container. We are not building a separate subsystem; we are simply describing the contract carefully.
Imagine a “good” fragment of src/main/resources/application.yml that fits the current goal:
spring:
application:
# Application name: useful in logs and in /actuator/info
name: catalog-service
management:
endpoints:
web:
exposure:
# Explicitly expose only the minimum required set of endpoints
include: "health,info,metrics"
info:
app:
# Pull the name from spring.application.name so we do not duplicate it by hand
name: "${spring.application.name}"
# Run mode: useful for seeing in /actuator/info which profile was chosen in the container
mode: "${SPRING_PROFILES_ACTIVE:standalone}"
Everything here is pretty boring — and that is great. Container operations love boredom: boredom means predictability.
Now that the container is running and the port is forwarded (for example, to 8080), we get three simple checks.
Health check:
curl -s http://localhost:8080/actuator/health
Passport check:
curl -s http://localhost:8080/actuator/info
Check that metrics are available (at least the list):
curl -s http://localhost:8080/actuator/metrics
If any of these checks do not work, you get an excellent diagnostic fork. If health does not work, that almost always means the application did not start correctly (and you need to look at the logs). If health works, but info/metrics do not, that is often just an exposure-setting issue. And that is exactly the moment when you stop “feeling Docker intuitively” and start acting like an engineer: you have a signal, you know where it should appear, and you check the configuration precisely.
As soon as /actuator/health already responds consistently, it becomes not only a human-readable URL for curl, but also an objective check for the containerized environment itself. In other words, it is already a machine-readable signal that can be used without reading long logs.
8. Typical mistakes
Mistake #1: confusing “the process is alive” with “the service is ready”.
A beginner looks at docker ps, sees the container in Up status, and concludes “everything works”. Then they try to open the API — and it does not respond. Actuator teaches the right check: the container may be running, but the application may not have initialized yet or may already have broken. If /actuator/health does not give UP, that is a signal not to argue with reality, but to go back to the logs and configuration.
Mistake #2: expecting /actuator/info and /actuator/metrics “to be available by themselves”.
A very common situation: health works, but the other endpoints return 404. Usually the problem is not Docker and not the ports, but the fact that the endpoints are not exposed externally. In this lecture, we fix the basic recipe: management.endpoints.web.exposure.include=health,info,metrics. If it is not there, “the room exists”, but “the door is locked”.
Mistake #3: exposing all Actuator endpoints without discrimination.
At first it seems logical: “well, I will expose * so it definitely works.” That is a dangerous habit. Some endpoints may reveal too much internal information, and some are not meant for “just taking a look” at all. In a training service, it is better to get used to a minimal set and strict exposure control — it is cheap discipline that later saves you from expensive surprises.
Mistake #4: putting sensitive data into /actuator/info.
Sometimes you want to add “useful” things to info: a database URL, a login, a host name. And then a password, a token, or part of a private infrastructure accidentally ends up there. With info, it is better to follow the “passport, not diary” model: name, version, mode. Anything that looks like a secret does not belong in info.
Mistake #5: trying to turn /actuator/health into a business-logic check.
The desire is understandable: “let's make health query the database, run an export, check Redis...” But that very quickly turns health into a heavy operation that starts breaking the application itself and producing false positives/negatives. In the course, we keep health light: it should be fast, short, and stable, otherwise it will not be a thermometer but a separate cause of illness.
GO TO FULL VERSION