Docker Compose Java Healthcheck
Docker compose is often used to run locally a development stack. Even if I would recommend to use minikube/microk8s/… + Yupiik Bundlebee, it is a valid option to get started quickly.
One trick is to handle dependencies between services.
A compose descriptor often looks like:
docker-compose.yaml
version: "3.9" (1) services: (2) postgres: (3) image: postgres:14.2-alpine restart: always ports: - "5432:5432" environment: POSTGRES_USERNAME: postgres POSTGRES_PASSWORD: postgres my-app-1: (4) image: my-app restart: always ports: - "18080:8080" my-app-2: (4) image: my-app restart: always depends_on: (5) - my-app-1
1 | the descriptor version |
2 | the list of services (often containers if there is no replicas) |
3 | some external images (often databases or transversal services like gateways) |
4 | custom application images |
5 | dependencies between images |
for web services it is not recommended having dependencies between services but it is insanely useful if you have a batch provisioning your database and you want it to run only when a web service is ready. It is often the case if you have a Kubernetes
CronJob
calling one of yourDeployment
/Service
.
Previous descriptor works but it can happen the web service is not fully started before the second app (simulating a batch/job) is launched.
To solve that we need to add a healthcheck on the first app and depend on the state of the application in the batch. Most of the examples will use curl
or wget
but it has the drawback to be forced to add these dependencies – and their dependencies – to the base image – don’t forget we want the image to be light – a bit for the size but generally more for security reasons – so that it shouldn’t be there.
So the overall trick will be to write a custom main
based on plain Java – since we already have a Java application.
Here is what can look like the modified docker-compose.yaml
file:
"my-app-1: ... healthcheck: (1) test: [ "CMD-SHELL", (2) "_JAVA_OPTIONS=", (3) "java", "-cp", "/opt/app/libs/my-jar-*.jar", (4) "com.app.health.HealthCheck", (5) "http://localhost:8080/api/health" (6) ] interval: 30s timeout: 10s retries: 5 start_period: 5s my-app-2: ... depends_on: my-app-1: condition: service_healthy (7)
1 | we register a healthcheck for the web service |
2 | we use CMD-SHELL and not CMD to be able to set environment variables in the command |
3 | we force the base image _JAVA_OPTION to be resetted to avoid to inherit the environment of the service (in particular if there is some debug option there) |
4 | we set the java command to use the jar containing our healthcheck main |
5 | we set the custom main we will write |
6 | we reference the local container health endpoint |
7 | on the batch service, we add the condition that the application must be service_healthy which means we control the state with the /health endpoint we have in the first application (and generally it is sufficient since initializations happen before it is deployed) |
Now, the only remaining step is to write this main com.app.health.HealthCheck
. Here is a trivial main class:
package com.app.health; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import static java.net.http.HttpResponse.BodyHandlers.discarding; public final class HealthCheck { private HealthCheck() { // no-op } public static void main(final String... args) throws IOException, InterruptedException { final var builder = HttpRequest.newBuilder() .GET() .uri(URI.create(args[0])); for (int i = 1; i < 1 + (args.length - 1) / 2; i++) { final var base = 2 * (i - 1) + 1; builder.header(args[base], args[base + 1]); } final var response = HttpClient.newHttpClient() .send(builder.build(), discarding()); if (response.statusCode() < 200 || response.statusCode() > 299) { throw new IllegalStateException("Invalid status: HTTP " + response.statusCode()); } } }
Nothing crazy there, we just do a GET
request on the based on the args
of the main. What is important to note there is you control that logic since you code the healthcheck so you can also check a file is present for example.
Last but not least you have to ensure the jar containing this class is in your docker image (generally the class can be included in a app-common.jar
) which will enable to reference it as classpath in the healthcheck command.
Indeed you can use any dependency you want if you also add them in the classpath of the healthcheck, but generally just using the JDK is more than sufficient and enables a simpler healthcheck command.
you can also build a dedicated healthcheck-main.jar archive and add it in your docker to use it directly. This option enables to set in the jar the Main-Class
which provides your the facility to use java -jar healthcheck-main.jar <url>
Published on Java Code Geeks with permission by Romain Manni, partner at our JCG program. See the original article here: Docker Compose Java Healthcheck Opinions expressed by Java Code Geeks contributors are their own. |