reading time: 15 minutes

Have you ever had that moment where a simple`./gradlew build`that you run a hundred times a day suddenly starts crashing with an error you’ve never seen? Multiply that by eleven successive errors, add a healthy dose of Docker Engine 29 incompatibility, sprinkle in some Gradle 9 making APIs you’ve used for ten years disappear. Here is the complete log.


Diagram 1 — Gradle 9 Migration (error chain)

Goal: show the series of incompatibilities.

Diagram
Figure 1. 👉 Clear, focused on Gradle.

Diagram 2 — Incompatible JHipster Plugins

Goal: isolate the broken plugins.

Diagram
Figure 2. 👉 Clean view of dependencies.

Diagram 3 — Docker 29 / Testcontainers

Goal: runtime problem.

Diagram
Figure 3. 👉 Clear history of the runtime.

Diagram
Figure 4. another way to represent it

The scene: a JHipster project, a build that no longer loads

The project`edster`is a JHipster application generated in 2024. The root build script`build.gradle`remained stable for months. Then we decided to upgrade:

Component

Target Version

Gradle

9.4.1

Java

21.0.11-tem (Eclipse Temurin)

JHipster

8.x (framework`tech.jhipster:jhipster-framework:8.11.0`)

Spring Boot

3.4.5

Kotlin

2.3.0

Docker Engine

29.4.1 (API 1.54)

The Java version change is handled via SDKMAN:

sdk use java 21.0.11-tem

First run:

$ ./gradlew build --no-daemon

FAILURE: Build failed with an exception.

* Where:
Build file '/home/.../edster/build.gradle' line: 18

* What went wrong:
An exception occurred applying plugin request [id: 'jhipster.cucumber-conventions']
> Failed to apply plugin 'jhipster.cucumber-conventions'.
   > Could not create task ':cucumberTest'.
      > Could not create task ':consoleLauncherTest'.
         > Could not set unknown property 'reportsDir' for task ':consoleLauncherTest'

We don’t even get past the build script loading. The problem is in`buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle`.

Error 1: the phantom variable reportsDir

The script`jhipster.cucumber-conventions.gradle`contains:

tasks.register('consoleLauncherTest', JavaExec) {
    dependsOn(testClasses)
    String cucumberReportsDir = file("$buildDir/reports/tests")
    outputs.dir(reportsDir)           // <-- reportsDir n'existe PAS
    classpath = sourceSets["test"].runtimeClasspath
    main = "org.junit.platform.console.ConsoleLauncher"
    // ...
}

The defined variable is`cucumberReportsDir`. The one used is`reportsDir`-- which is not defined anywhere.

Correction:`outputs.dir(cucumberReportsDir)`

Error 2: main = "…​" no longer exists under Gradle 9

Immediate restart:

Could not set unknown property 'main' for task ':consoleLauncherTest' of type org.gradle.api.tasks.JavaExec.

Gradle 9 removes the property`main`in favor of`mainClass`on`JavaExec`tasks.

Correction:`mainClass = "org.junit.platform.console.ConsoleLauncher"`

Error 3: sourceCompatibility is no longer a project property

New error:

Could not set unknown property 'sourceCompatibility' for root project 'edster' of type org.gradle.api.Project.

The root script had:

sourceCompatibility=17
targetCompatibility=17

Gradle 9 removes these properties at the root level. They must reside in a`java`block.

Correction:

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

Error 4: the three-operand Groovy assertion

The old script contained:

assert System.properties["java.specification.version"] == "17" || "21" || "24"

In Groovy,"21" et "24"`are truthy strings. The expression resolves to(…​ == "17") || true || true`, which is always true — but the syntax is invalid for the desired strict assertion.

Correction:

assert System.properties["java.specification.version"] in ["17", "21", "23", "24"]

Error 5: the implicit dependency compileKotlinopenApiGenerate

The build progresses. Kotlin compilation successful. Then:

Task ':compileKotlin' uses this output of task ':openApiGenerate'
without declaring an explicit or implicit dependency.

Gradle 9 refuses implicit dependencies between tasks. Since`compileKotlin`reads the sources generated by`openApiGenerate`, the link must be declared.

Correctionin`build.gradle`:

afterEvaluate {
    tasks.named("compileKotlin").configure {
        dependsOn(tasks.named("openApiGenerate"))
    }
}

Le `afterEvaluate`is necessary because`openApiGenerate`is an extension (openAPI Generator plugin) and not a task directly accessible during the configuration phase.

Error 6: generateGitProperties breaks on FilterOutputStream.write()

Java compilation passes. Resources are processed. Then:

> Task :generateGitProperties FAILED

No signature of method: java.io.FilterOutputStream.write() is applicable for argument types: (Integer) values: [103]

The`gradle-git-properties`plugin version 2.5.0 is incompatible with Java 21 / Gradle 9. An internal bug attempts to call`write(int)`via Groovy on a stream that has been closed.

Correctionin`gradle.properties`:

gitPropertiesPluginVersion=2.5.7

Finally: Testcontainers enters the scene

Until now, it was pure "build script debugging." Each error was a Gradle 9 or Java 21 incompatibility in the build scripts. After the six corrections above, the`./gradlew assemble`passes successfully.

But`./gradlew build`also executes tests, and Cucumber tests use Testcontainers to launch an ephemeral PostgreSQL.

Could not find a valid Docker environment.

EnvironmentAndSystemPropertyClientProviderStrategy: failed with exception BadRequestException
(Status 400: {"message":"client version 1.32 is too old. Minimum supported API version is 1.40"}
UnixSocketClientProviderStrategy: failed with exception BadRequestException
(Status 400: {"message":"client version 1.32 is too old. Minimum supported API version is 1.40"}

Testcontainers is unable to communicate with Docker. Two different strategies (environment variables + Unix socket) fail with the same error.

Handshake Docker API refusé par le daemon

Phase 1: the docker-java lead

First reflex: the Java Docker client used by Testcontainers sends API version`1.32`in its HTTP handshake, but Docker Engine 29.4.1 requires a minimum of`1.40`. The problem is at the client level, not the daemon.

Verification of the docker-java version resolved by Testcontainers 1.20.6:

$ ./gradlew dependencies --configuration testRuntimeClasspath | grep docker-java

+--- com.github.docker-java:docker-java-api:3.4.1
+--- com.github.docker-java:docker-java-transport:3.4.1

docker-java 3.4.1, dating from Testcontainers 1.20.6. The latest public version of docker-java is 3.5.1. Let’s try to force the version upgrade via`resolutionStrategy`in the`configurations` de `build.gradle`block:

configurations {
    all {
        resolutionStrategy.eachDependency { details ->
            if (details.requested.group == "com.github.docker-java"
                && details.requested.name.startsWith("docker-java")) {
                details.useVersion("3.5.1")
            }
        }
    }
}

Rerunning the build. Same error:`client version 1.32 is too old`.

Verifying the Gradle cache: the JARs were indeed re-downloaded, but the error persists. docker-java 3.5.1 still sends`1.32`. This version is therefore NOT sufficient for Docker Engine 29.4.1.

Purification action: completely clear the Gradle cache, just to be sure:

rm -rf ~/.gradle/caches
rm -rf /path/to/project/.gradle

Complete restart after purge. Same error.

Phase 2: web search and GitHub breadcrumbs

docker-java 3.5.1 is insufficient. Nothing left but to search if someone has encountered this exact problem.

Targeted search on testcontainers-java and docker-java issues:

site:github.com/testcontainers "client version 1.32 is too old"
site:github.com/docker-java "client version 1.32" Docker Engine 29

Immediate first results:

The breadcrumbs are clear: Docker Engine 29.x raised its minimum API version from`1.24`(former default) to`1.40`. Testcontainers 1.20.x uses docker-java 3.4.x which negotiates the API version by sending`1.32`. The daemon politely but firmly refuses.

In issue #11491, a maintainer responds:

_ Hi, I have a project running with version 2.0.3 on a GH runner with engine 29.1 and it works. Can you all please check there is no version conflict? Please, remember all modules were prefixed with`testcontainers-. So, starting with version 2.x we went from`postgresql to testcontainers-postgresql _

This response containstwo critical pieces of information:

  1. Testcontainers 2.0.3+ resolves the problem

  2. The modules have beenrenamed with the testcontainers- prefix

Phase 3: the trap of renamed artifacts

The renaming is brutal. In Testcontainers 2.x, none of the old names work under that name anymore:

Old name (1.x)

New name (2.x)

org.testcontainers:postgresql

org.testcontainers:testcontainers-postgresql

org.testcontainers:jdbc

org.testcontainers:testcontainers-jdbc

org.testcontainers:junit-jupiter

org.testcontainers:testcontainers-junit-jupiter

org.testcontainers:testcontainers

unchanged

First attempt: apply the Testcontainers 2.0.5 BOM and the new artifact names in`build.gradle`.

dependencies {
    testImplementation platform("org.testcontainers:testcontainers-bom:2.0.5")
    testImplementation "org.testcontainers:testcontainers-postgresql"
    testImplementation "org.testcontainers:testcontainers-jdbc"
    testImplementation "org.testcontainers:testcontainers-junit-jupiter"
    testImplementation "org.testcontainers:testcontainers"
}

./gradlew build

FAILURE: Could not find org.testcontainers:jdbc:2.0.5.
Could not find org.testcontainers:junit-jupiter:2.0.5.

The testcontainers-bom BOM is not enough.Why? Because Spring Boot 3.4.5 exposes its own dependency management BOM (spring-boot-dependencies) whichwinsover the Testcontainers BOM in transitive resolution. Spring Boot fixes`testcontainers` à 1.20.6, and thus all artifacts not explicitly in the Spring Boot BOM (like the new`testcontainers-*) resolve to the old names`1.20.6-- which no longer exist in this version.

Inspecting the`dependencies --configuration testRuntimeClasspath`, we find:

+--- org.testcontainers:testcontainers -> 1.20.6 (*)
|    \--- org.testcontainers:testcontainers:2.0.5 -> 1.20.6

Le →`indicates the substitution: Testcontainers 2.0.5 is requested, but Spring Boot forces`1.20.6.

Conflit entre le BOM Spring Boot et le BOM Testcontainers

Phase 4: forcing the resolution

The strategy becomes twofold:

  1. Force docker-java to its 3.7.1 version (tested as compatible with Docker Engine 29)

  2. Force Testcontainers core to 2.0.5 by bypassing the Spring Boot BOM

resolutionStrategy.eachDependency court-circuite le BOM Spring Boot

Final correctionin`build.gradle`:

configurations {
    all {
        resolutionStrategy.eachDependency { details ->
            if (details.requested.group == "com.github.docker-java"
                && details.requested.name.startsWith("docker-java")) {
                details.useVersion("3.7.1")
            }
            if (details.requested.group == "org.testcontainers"
                && details.requested.name == "testcontainers") {
                details.useVersion("2.0.5")
            }
        }
    }
}

dependencies {
    testImplementation platform("org.testcontainers:testcontainers-bom:2.0.5")
    testImplementation "org.testcontainers:testcontainers-jdbc"
    testImplementation "org.testcontainers:testcontainers-junit-jupiter"
    testImplementation "org.testcontainers:testcontainers-postgresql"
    testImplementation "org.testcontainers:testcontainers"
    // ... autres dépendances Spring Boot
}

Le `resolutionStrategy.eachDependency`is the only way to override the`spring-boot-dependencies`BOM managed by the Spring Boot plugin. The equality test on`details.requested.name == "testcontainers"`is intentionally restrictive: we only force the core module. The`testcontainers-*`modules follow the Testcontainers 2.0.5 BOM that we explicitly declared.

Final result

$ ./gradlew build --no-daemon

> Task :consoleLauncherTest
[2 containers found]
[2 containers started]
[2 containers successful]

BUILD SUCCESSFUL in 1m 16s

Testcontainers 2.0.5 + docker-java 3.7.1 finally manage to create their PostgreSQL containers via Docker Engine 29.4.1. The final business error (HTTP 500 on the Cucumber test) is an application problem totally independent of the build script.

Summary table of corrections

# File Problem Correction

1

buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle

Non-existent`reportsDir`variable

outputs.dir(cucumberReportsDir)

2

buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle

`main`Gradle 9 deprecated

mainClass

3

build.gradle

`sourceCompatibility`top-level forbidden Gradle 9

Bloc java { sourceCompatibility = JavaVersion.VERSION_21 }

4

build.gradle

Syntactically invalid Groovy assertion

assert …​ in ["17", "21", "23", "24"]

5

build.gradle

Implicit dependency`compileKotlin`→openApiGenerate

afterEvaluate { tasks.named("compileKotlin").dependsOn("openApiGenerate") }

6

gradle.properties

`gitPropertiesPluginVersion`Java 21 incompatible

2.5.7

7

Gradle cache

docker-java 3.5.1 tested, resolved but insufficient

Purge + move to docker-java 3.7.1

8

build.gradle

Testcontainers 1.20.6 incompatible with Docker Engine 29

Force Testcontainers 2.0.5 + prefix`testcontainers-*`+ resolutionStrategy vs Spring Boot BOM

Conclusion

If your JHipster/Gradle build crashes after upgrading to Gradle 9 + Docker Engine 29, the errors fall into two categories:

Build script errors(the first 6): Gradle 9 removes properties and syntaxes that had worked for years (sourceCompatibility=, main =, reportsDir). These are mechanical migrations once you know the new API.

Docker runtime errors(the last 2): Docker Engine 29.4.1 rejects API clients < 1.40. Testcontainers 1.20.6 (imposed by Spring Boot 3.4.5) uses docker-java 3.4.1 which sends 1.32. Only Testcontainers 2.0.5 + docker-java 3.7.1 resolves the problem, with artifact renaming and version forcing via`resolutionStrategy.eachDependency`as the only way to bypass the implicit Spring Boot BOM.

The Gradle build script at the project root is now clean.`./gradlew build`passes on Java 21, Gradle 9.4.1, Spring Boot 3.4.5, and Docker Engine 29.4.1.

Related articles