When you launch a container, Docker maintains its status as it transitions between several states. When you list the containers (using docker ps
or docker container ls
), the status of those states is displayed under CREATED
and STATUS
labels. These states are tracked through the server events; you can list them in real-time using the docker events
command.
For our usecase, consider an example of a Spring Boot application that uses
- Tomcat as the embedded server, by including
spring-boot-starter-web
as a dependency - Spring Boot Actuator to expose a health endpoint that shows application health information, by including
spring-boot-starter-actuator
as a dependency
You can create a Docker image of the above application (with docker build . -t endpoint
command) using the following Dockerfile.
FROM adoptopenjdk/openjdk11:alpine-jre
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
COPY target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
When you launch the container of the above-mentioned image (e.g., through docker run -p 8080:8080 endpoint:latest
) and list it using docker ps
, you may get a listing like this.
$ docker ps --filter ancestor=endpoint:latest
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES"java -jar app.jar" 2 minutes ago Up 2 minutes 0.0.0.0:8080->8080/tcp elastic_cray
2adf5b17ceb0 endpoint:latest
Even though this is a lot of useful information, you get no idea whether or not the Tomcat server is up until you access the application itself or ping the health endpoint (i.e., http://localhost:8080/actuator/health
by default) exposed by the actuator. Wouldn’t it be useful if Docker knows about the status through this health endpoint and uses this information to determine if the container is healthy?
Docker HEALTHCHECK
instruction
That’s where the Docker HEALTHCHECK
instruction comes into the picture. When the container is up and running, you can ping the health endpoint and get the status of the application as follows.
$ curl http://localhost:8080/actuator/health"status":"UP"}
{
You can add a rudimentary health check using this endpoint in the Dockerfile as follows.
FROM adoptopenjdk/openjdk11:alpine-jre
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
HEALTHCHECK --interval=25s --timeout=3s --retries=2 CMD wget --spider http://localhost:8080/actuator/health || exit 1
Here, the HEALTHCHECK
instruction specifies that after 25 seconds when the container has started, the command wget --spider http://localhost:8080/actuator/health || exit 1
should be executed. Docker will wait for 3 seconds for the command to return and retry 2 times if it fails on previous tries. If the final exit code is 1, the container will be marked as unhealthy
; if the exit code is 0, it will be marked as healthy
. This status will appear when you execute docker ps
.
$ docker ps --filter ancestor=endpoint:latest
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES"java -jar app.jar" 22 seconds ago Up 21 seconds (health: starting) 0.0.0.0:8080->8080/tcp gallant_dijkstra
620970808ed0 endpoint:latest
$ docker ps --filter ancestor=endpoint:latest
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES"java -jar app.jar" 49 seconds ago Up 48 seconds (healthy) 0.0.0.0:8080->8080/tcp gallant_dijkstra
620970808ed0 endpoint:latest
I’m using
wget
here but you can also usecurl
if it is available in your container.
This way Docker’s HEALTHCHECK
instruction bubbles up the health information by telling Docker if the container is working.
The biggest problem with this specific approach is that it relies on wget
or curl
. What if none of these utilities are available in the container? This is a possible scenario when you use a distroless image; the only thing that would be available in such an image would be the Java tooling (JDK or JRE). How can we create a health check in such a scenario? This post explores the solution to this question.
Single-file programs using Java 11
Java 11 introduced the ability to run a Java program in a single file directly through java
command without compiling it using javac
. In short, you can write a Java program like this one,
public class Greeter {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
and run it by executing the following command.
$ java Greeter.java!
Hello, world
Please note that the java
command uses the Java compiler (available in the jdk.compiler
module) to compile and run a single-file program under the hood. This is essential to note because many Docker images based on the JRE don’t ship with the jdk.compiler
module. In such cases, you’ll have to compile the single-file program beforehand to run it in the container.
Java HTTP Client
Java 11 also introduced an HTTP Client to request HTTP resources over the network. You can use this to call the actuator endpoint and perform some validations on the response. Here’s an example.
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
public class HealthCheck {
public static void main(String[] args) throws InterruptedException, IOException {
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
URI.create("http://localhost:8080/actuator/health"))
.uri("accept", "application/json")
.header(
.build();
var response = client.send(request, BodyHandlers.ofString());
if (response.statusCode() != 200 || !response.body().contains("UP")) {
throw new RuntimeException("Healthcheck failed");
}
}
}
In the above program, we
- make a call to the actuator endpoint using the
HttpClient
- verify if the status code of the response is 200 and if the response contains the string
UP
or not
The BodyHandlers
class provides several static methods that return useful HttpResponse.BodyHandler
instances. The BodyHandlers.ofString()
is one such method that returns a HttpResponse.BodyHandler<String>
from which the response body can be accessed as a String
.
You can also implement the
HttpResponse.BodyHandler
interface to deserialize the response into a Java object and perform even fancier validations on the response (e.g., database and filesystem health checks).
Docker HEALTHCHECK
instruction using a single-file Java program
Using the HealthCheck
single-file Java program above, we can declare a HEALTHCHECK
instruction for a distroless image as follows.
FROM gcr.io/distroless/java:11
COPY target/*.jar app.jar
COPY HealthCheck.java .
EXPOSE 8080
CMD ["app.jar"]
HEALTHCHECK --interval=25s --timeout=3s --retries=2 CMD ["java", "HealthCheck.java", "||", "exit", "1"]
While using a distroless image, you need to specify the ENTRYPOINT
or CMD
commands in their exec (JSON array) form because the distroless images don’t contain a shell to launch.
Once you launch the container, you can check the status through docker ps
as follows.
$ docker ps --filter ancestor=endpoint:latest
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES"/usr/bin/java -jar …" 24 seconds ago Up 24 seconds (health: starting) 0.0.0.0:8080->8080/tcp serene_cerf
769e8404b96e endpoint:latest
$ docker ps --filter ancestor=endpoint:latest
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES"/usr/bin/java -jar …" About a minute ago Up About a minute (healthy) 0.0.0.0:8080->8080/tcp serene_cerf
769e8404b96e endpoint:latest
This approach eliminates the need for any utility (like curl
or wget
) and relies solely on the Java tooling available in the image. It is also portable because it works irrespective of the operating system or underlying base of the image.
Source code
Related
- Dockerfile HEALTHCHECK reference
- Running Single-file Programs without Compiling in Java 11
- Introduction to the Java HTTP Client