Core Java

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
1the descriptor version
2the list of services (often containers if there is no replicas)
3some external images (often databases or transversal services like gateways)
4custom application images
5dependencies 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 your Deployment/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)
1we register a healthcheck for the web service
2we use CMD-SHELL and not CMD to be able to set environment variables in the command
3we 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)
4we set the java command to use the jar containing our healthcheck main
5we set the custom main we will write
6we reference the local container health endpoint
7on 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.

Romain Manni-Bucau

Romain is a senior software engineer with a deep background on technical stacks from the standalone to cloud native applications without forgetting Bi Data. He also contributes a lot to the Open Source ecosystem (Apache, Yupiik, ...)
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button