reading time: 15 minutes

You have already experienced that moment when a simple`./gradlew build`You launch it a hundred times a day, and suddenly it crashes on an error never seen before? Multiply that by eleven successive errors, add a healthy dose of Docker Engine 29 incompatibility, sprinkle in Gradle 9 that makes APIs you used for ten years disappear. Here’s the full log.


Diagram 1 — Gradle 9 migration (error chain)

Goal: show the series of incompatibilities.

👉 Clear, centered on Gradle.
@startuml
left to right direction
title Migration Gradle 9

rectangle "1\nreportsDir" as A
rectangle "2\nmainClass" as B
rectangle "3\nsourceCompatibility" as C
rectangle "4\nGroovy assert" as D

A --> B
B --> C
C --> D

@enduml

Diagramme 2 — Incompatible JHipster plugins

Objective: isolate the broken plugins.

👉 Clear vision of dependencies.
@startuml
left to right direction
title Plugins to fix
skinparam shadowing false
skinparam roundcorner 18

rectangle "1\nopenApiGenerate" as A #FFF3E0
rectangle "2\ngitProperties\n2.5.7" as B #FFF3E0
rectangle "3\nCorrected\nversions" as C #E3F2FD
rectangle "4\nAssemble OK" as D #E8F5E9

A --> B
B --> C
C --> D

@enduml

Diagram 3 — Docker 29 / Testcontainers

Objective: runtime problem.

👉 Clear history of the runtime.
@startuml
left to right direction
title Docker 29 + Testcontainers

skinparam shadowing false
skinparam roundcorner 18
skinparam defaultFontSize 14

rectangle "1\nAssemble OK" as A #E8F5E9
rectangle "2\nCucumber Tests FAIL" as B #FFEBEE
rectangle "3\nTestcontainers 1.32\nrejected" as C #FFF3E0
rectangle "4\nUpgrade\nversions" as D #E3F2FD
rectangle "5\nFinal build OK" as E #E8F5E9

A --> B
B --> C
C --> D
D --> E

@enduml

another way of representing it
@startuml

!option handwritten true

skinparam backgroundColor #FEFEFE
skinparam shadowing false
skinparam defaultFontName Inter
skinparam defaultFontSize 14
skinparam ArrowThickness 2
skinparam RoundCorner 18

title Stabilization Roadmap — Gradle 9 to Final Build
left to right direction

rectangle "⚙️ Gradle 9 Migration" as A #E3F2FD
rectangle "🧩 JHipster Plugins" as B #FFF3E0
rectangle "🐳 Docker 29\nTestcontainers" as C #FCE4EC
rectangle "✅ Stable Build" as D #E8F5E9

note bottom of A
reportsDir
mainClass
sourceCompatibility
Groovy assert
end note

note bottom of B
openApiGenerate
gitProperties
versions alignées
end note

note bottom of C
tests Cucumber KO
image Docker rejetée
upgrade libs
end note

A -[#42A5F5]-> B : corrections build.gradle
B -[#FB8C00]-> C : assemble OK
C -[#E53935]-> D : tests réparés

@enduml

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 decide to upgrade the version:

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)

Changing the Java version goes through SDKMAN :

sdk use java 21.0.11-tem

First launch:

$ ./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 off loading the build script. 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 used one is`reportsDir`-- which is not defined anywhere.

Correction:`outputs.dir(cucumberReportsDir)`

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

Immediate follow-up:

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

Gradle 9 removes the property`main`for the benefit of`mainClass`on the tasks`JavaExec`.

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 live in a block`java`.

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 strict assertion desired.

Correction:

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

Error 5: the implicit dependency compileKotlin → `openApiGenerate

The build is progressing. Kotlin compilation succeeded. 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`, you must declare the link.

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()

The 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 plugin`gradle-git-properties`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 closed it.

Correctionin`gradle.properties`:

gitPropertiesPluginVersion=2.5.7

Finally: Testcontainers steps onto the stage

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

But`./gradlew build`It also runs the tests, and the Cucumber tests use Testcontainers to start 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.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false

actor "Testcontainers\n1.20.6" as TC
participant "docker-java\n3.4.1" as DJ
participant "Docker Engine\n29.4.1" as DE

TC -> DJ : new DockerClient()
DJ -> DE : GET /_ping\nUser-Agent: docker-java/3.4.1\nAPI-Version: 1.32
DE --> DJ : HTTP 400 Bad Request\nclient version 1.32 is too old.\nMinimum supported API version is 1.40
DJ --> TC : BadRequestException
TC --> TC : Could not find a valid\nDocker environment

note right of DE
  Docker Engine 29.x impose
  API minimum = 1.40
  (défaut ancien : 1.24)
end note

note left of DJ
  docker-java 3.4.1 envoie
  hardcoded API version 1.32
  dans l'en-tête HTTP
end note

@enduml

Phase 1: the docker-java track

First reflex: the Java Docker client used by Testcontainers sends the 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.

Checking 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 block`configurations` de build.gradle(Empty output)

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

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

Checking Gradle cache: the JARs have indeed been 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

Full restart after purge. Same error.

Phase 2: web search and GitHub breadcrumb

docker-java 3.5.1 is insufficient. All that remains is to check if anyone 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

First immediate results:

The breadcrumb is clear: Docker Engine 29.x has raised its minimum API version to`1.24`(old defect) 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 replies:

</think>

This response containstwo critical pieces of information :

  1. Testcontainers 2.0.3+ solves the problem

  2. The modules have beenrenamed with the prefix `testcontainers-

Phase 3: the trap of renamed artifacts

Renaming is brutal. In Testcontainers 2.x, none of the old names work under that name any longer:

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) whowinson the Testcontainers BOM in transitive resolution. Spring Boot sets`testcontainers` à 1.20.6, and therefore all artifacts that are not explicitly in the Spring Boot BOM (such as the new`testcontainers-*) resolve to the old names`1.20.6-- that no longer exist in this version

Inspecting the`dependencies --configuration testRuntimeClasspath`, one finds:

+--- 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.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false
skinparam componentStyle rectangle

left to right direction

component "build.gradle\n(consumer)" as BG #E3F2FD {
    rectangle "request\n`testcontainers-bom:2.0.5" as REQ
    rectangle "request\n`testcontainers-postgresql" as REQ2
}

component "Maven Central" as MC #FFF3E0

component "Declarations" as DEC #E8F5E9 {
    rectangle "testcontainers-bom:2.0.5`\nfixes 2.0.5" as BOM2
    rectangle "spring-boot-dependencies:3.4.5`\nsets 1.20.6" as BOM1
}

component "Gradle Resolution" as RES #FFEBEE {
    rectangle "Winning version\n`testcontainers:1.20.6" as WIN #EF9A9A
}

BG --> DEC : "analyze the BOMs"
BOM2 --> RES : "proposes 2.0.5"
BOM1 --> RES : "propose 1.20.6"
WIN -> MC : "download\ntestcontainers:1.20.6"

note bottom of WIN
  Spring Boot BOM prime car
  le plugin spring-boot applique
  ses dépendances de gestion
  après la résolution du consommateur
end note

@enduml

Phase 4: force the resolution

The strategy becomes twofold:

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

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

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false
skinparam componentStyle rectangle

left to right direction

component "build.gradle\n(with resolutionStrategy)" as BG #E8F5E9 {
    rectangle "Testcontainers BOM\n`2.0.5" as BOM2
    rectangle "resolutionStrategy\n.eachDependency" as RS #66BB6A
}

component "Conflict resolved" as RESOLVED #E3F2FD {
    rectangle "docker-java\n`3.7.1` (forced)" as DJ
    rectangle "testcontainers\n`2.0.5` (forced)" as TC #81C784
    rectangle "testcontainers-postgresql\n`2.0.5" as TPG
    rectangle "testcontainers-jdbc\n`2.0.5" as TJ
    rectangle "testcontainers-junit-jupiter\n`2.0.5" as TJJ
}

component "Spring Boot BOM\n(implicit, overridden)" as BOM1 #FFEBEE

component "Maven Central" as MC #FFF3E0

BG --> RESOLVED : "BOM Testcontainers 2.0.5\ngoes directly"
RS -> DJ : "force 3.7.1"
RS -> TC : "force 2.0.5"
RESOLVED --> MC : "download the JARs\ncompatible with Docker 29"
BOM1 -[#gray,dashed]-> RESOLVED : "ignored on testcontainers\nand docker-java"

note bottom of RS
  Le `eachDependency` intercepte
  la résolution Gradle AVANT
  que le BOM Spring Boot ne gagne.
  C'est une intervention chirurgicale.
end note

@enduml

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 bypass the BOM`spring-boot-dependencies`managed by the Spring Boot plugin. The equality test on`details.requested.name == "testcontainers"`is intentionally restrictive : we only force the core module. The modules`testcontainers-*`That follow the Testcontainers 2.0.5 BOM we explicitly declared.

Result final

$ ./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 a purely application problem completely independent of the build script.

Summary table of corrections

# File Problem Correction

1

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

Variable`reportsDir`nonexistent

outputs.dir(cucumberReportsDir)

2

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

`main`deprecated Gradle 9

mainClass

3

build.gradle

`sourceCompatibility`top-level forbidden Gradle 9

Block`java { sourceCompatibility = JavaVersion.VERSION_21 }`

4

build.gradle

Invalid Groovy assertion syntax

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

5

build.gradle

Implicit dependency`compileKotlin` → openApiGenerate

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

6

gradle.properties

`gitPropertiesPluginVersion`incompatible Java 21

2.5.7

7

Cache Gradle

docker-java 3.5.1 tested, resolved but insufficient

Purge + passage docker-java 3.7.1

8

build.gradle

Testcontainers 1.20.6 incompatible with Docker Engine 29

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

Conclusion

If your JHipster/Gradle build crashes after an upgrade 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 been working for years.sourceCompatibility=, main =, reportsDir). They are mechanical migrations once you know the new API.

Docker runtime errors(the last two): Docker Engine 29.4.1 discards 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 solves the problem, with the renaming of artifacts and forcing version via`resolutionStrategy.eachDependency`as the only way to bypass Spring Boot’s implicit BOM.

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

Related articles