Testcontainers on Docker 29: fix client version 1.32 is too old

You upgrade Docker to 29, and suddenly every Testcontainers test fails before a single container starts. The log shows UnixSocketClientProviderStrategy: failed with exception BadRequestException followed by client version 1.32 is too old. Minimum supported API version is 1.44. Your test code didn’t change, which makes the cause hard to spot.

A Docker 29.0.0 daemon rejects requests below API 1.44 with a 400, and Testcontainers 2.0.1 and 1.21.3 connect with API 1.32.
Upgrade Testcontainers to 2.0.2 or later (1.21.4 or later on 1.x). I confirmed that all four versions from 2.0.2 to 2.0.5 pass.
If you can’t upgrade yet, -Dapi.version=1.44 or the daemon’s DOCKER_MIN_API_VERSION gets you through, but treat both as stopgaps.

Environment

To match a Linux host, I ran Docker 29.0.0 as a docker:29.0.0-dind container. A Maven container shares that daemon’s /var/run/docker.sock and runs the tests. I also ran the same test separately against Docker Desktop 29.9.2 on Windows for comparison.

Item Version
Docker Engine (reproduction target) 29.0.0, API 1.52 (minimum version 1.44)
Docker Desktop (comparison) Engine 29.9.2, API 1.56 (minimum 1.40)
JDK 21.0.12.1 (Temurin)
Maven 3.10.0
Testcontainers 2.x 2.0.1 (reproduction), 2.0.2, 2.0.3, 2.0.4, 2.0.5 (final)
Testcontainers 1.x 1.21.3 (reproduction), 1.21.4
PostgreSQL image postgres:17.6-alpine

Reproduce: Testcontainers 2.0.1 fails with BadRequestException on Docker 29.0.0

The lab puts the Docker daemon and the Maven runner in one Compose file. The docker service shares /var/run through a volume, so Testcontainers connects over the Unix socket exactly like it would on a Linux host.

services:
  docker:
    image: docker:29.0.0-dind
    privileged: true
    environment:
      DOCKER_TLS_CERTDIR: ""
    volumes:
      - docker-sock:/var/run
    healthcheck:
      test: ["CMD", "docker", "info"]
      interval: 2s
      timeout: 5s
      retries: 30

  maven:
    image: maven:3.10.0-eclipse-temurin-21
    network_mode: "service:docker"
    working_dir: /app
    environment:
      # mapped ports are published inside the dind network namespace
      TESTCONTAINERS_HOST_OVERRIDE: localhost
    volumes:
      - .:/app
      - ./.tools/m2:/root/.m2
      - docker-sock:/var/run
    depends_on:
      docker:
        condition: service_healthy

volumes:
  docker-sock:

healthcheck and depends_on hold the tests back until the dind daemon answers docker info. Without them, the tests could start before the daemon is up, and you’d get connection failures that have nothing to do with the API version.

network_mode: "service:docker" lets the Maven container reach the mapped ports of Testcontainers-managed containers on localhost. On a real Linux host you don’t need any of this; Testcontainers just uses /var/run/docker.sock.

The test starts a PostgreSQL container and runs select version() once. That’s all.

@Testcontainers
class PostgresContainerTest {

    @Container
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:17.6-alpine");

    @Test
    void selectVersion() throws Exception {
        try (Connection conn = DriverManager.getConnection(
                postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
             ResultSet rs = conn.createStatement().executeQuery("select version()")) {
            rs.next();
            String version = rs.getString(1);
            System.out.println("DB version: " + version);
            assertTrue(version.startsWith("PostgreSQL 17"));
        }
    }
}

In Testcontainers 2.x, the PostgreSQL module lives in the org.testcontainers.postgresql package. The 1.x comparison project uses org.testcontainers.containers.PostgreSQLContainer instead.

Pin Testcontainers to 2.0.1 and the run fails before any container is created.

docker compose run --rm maven mvn -B -f tc2/pom.xml -Dtestcontainers.version=2.0.1 test
[main] INFO org.testcontainers.DockerClientFactory - Testcontainers version: 2.0.1
[main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
	UnixSocketClientProviderStrategy: failed with exception BadRequestException (Status 400: {"message":"client version 1.32 is too old. Minimum supported API version is 1.44, please upgrade your client to a newer version"}
)
	DockerDesktopClientProviderStrategy: failed with exception NullPointerException (Cannot invoke "java.nio.file.Path.toString()" because the return value of "org.testcontainers.dockerclient.DockerDesktopClientProviderStrategy.getSocketPath()" is null)As no valid configuration was found, execution cannot continue.
...
java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration
	at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:274)
...
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

The exception ends with IllegalStateException: Could not find a valid Docker environment, so it’s easy to assume Docker isn’t running. The real cause is the 400 response on the UnixSocketClientProviderStrategy line just above it. The NullPointerException from DockerDesktopClientProviderStrategy below that only means there’s no Docker Desktop socket on Linux. You can ignore it.

The 1.x line behaves the same way. With Testcontainers 1.21.3, the UnixSocketClientProviderStrategy line prints the same client version 1.32 is too old message and the build ends in BUILD FAILURE.

Cause: Docker 29.0.0 raised the minimum API version to 1.44

The Docker 29.0.0 release notes include daemon: raise minimum API version to v1.44 (moby/moby#51186). Before that, the daemon accepted API versions down to 1.24. The daemon in the lab reports the new minimum in docker version.

Server: Docker Engine - Community
 Engine:
  Version:          29.0.0
  API version:      1.52 (minimum version 1.44)

When the docker-java configuration has no API version set, Testcontainers 2.0.1 fills in RemoteApiVersion.VERSION_1_32 as the default. PR #11216, which shipped in 2.0.2, changes that default to VERSION_1_44. In other words, Testcontainers doesn’t negotiate a version with the daemon. It puts a fixed version in the request path.

To confirm that the daemon rejects requests based on the API version alone, I sent two curl requests to the same socket. /v1.32/version returns a 400 with the same message as the test log.

docker compose run --rm --no-deps maven curl -sS -i --unix-socket /var/run/docker.sock http://localhost/v1.32/version
HTTP/1.1 400 Bad Request
Api-Version: 1.52
Content-Type: application/json
Ostype: linux
Server: Docker/29.0.0 (linux)
...
{"message":"client version 1.32 is too old. Minimum supported API version is 1.44, please upgrade your client to a newer version"}

Change only the path to /v1.44/_ping, and the daemon returns HTTP/1.1 200 OK with a body of OK. The test code and the image don’t matter here. The API version the client puts in the path decides the outcome.

On Docker Desktop, Status 400 is followed by empty JSON

Docker Desktop shows a different error. When I ran the same 2.0.1 test against Docker Desktop on Windows (Engine 29.9.2), NpipeSocketClientProviderStrategy failed. The response body has no too old text, only JSON with empty values.

[main] INFO org.testcontainers.DockerClientFactory - Testcontainers version: 2.0.1
[main] ERROR org.testcontainers.dockerclient.DockerClientProviderStrategy - Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
	NpipeSocketClientProviderStrategy: failed with exception BadRequestException (Status 400: {"ID":"","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"", ... "Labels":["com.docker.desktop.address=npipe://\\\\.\\pipe\\docker_cli"], ... "Warnings":null})As no valid configuration was found, execution cannot continue.

The API version is still the cause. On the same Docker Desktop, adding only -Dapi.version=1.44 to the 2.0.1 run made it pass, and upgrading to 2.0.5 passed too. If you see an error that starts with Status 400: {"ID":"",...}, check your Testcontainers version first.

The Docker 29.3.0 release notes include Lower minimum API version from v1.44 to v1.40 (Docker 19.03). The 29.9.2 host engine did report a minimum of 1.40. But 1.32 is lower than 1.40, too. So moving Docker to a 29.x release after 29.3 doesn’t save Testcontainers 2.0.1 or 1.21.3. Waiting for Docker to relax the limit isn’t a fix.

Fix: upgrade to Testcontainers 2.0.2+ (or 1.21.4+ on 1.x)

Upgrading Testcontainers is the reliable fix. The 2.0.2 release notes list Set default docker API version to 1.44 (#11216), and the 1.21.4 release notes say This release makes version 1.21.x works with recent Docker Engine changes. The lab pom.xml manages the BOM version with a single property.

<properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- 2.0.1 fails on Docker 29.0.0, 2.0.2+ works -->
    <testcontainers.version>2.0.5</testcontainers.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-bom</artifactId>
            <version>${testcontainers.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

The module dependencies (testcontainers-postgresql, testcontainers-junit-jupiter) have no version of their own and inherit it from the BOM. That leaves exactly one line to change.

Here are the results per version against the Docker 29.0.0 daemon:

Testcontainers Result
1.21.3 Fails — client version 1.32 is too old
1.21.4 Passes
2.0.1 Fails — client version 1.32 is too old
2.0.2 Passes
2.0.3 Passes
2.0.4 Passes
2.0.5 Passes

With 2.0.5, Testcontainers finds the Unix socket and reads the server version.

[main] INFO org.testcontainers.DockerClientFactory - Testcontainers version: 2.0.5
[main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
[main] INFO org.testcontainers.DockerClientFactory - Docker host IP address is localhost
[main] INFO org.testcontainers.DockerClientFactory - Connected to docker: 
  Server Version: 29.0.0
  API Version: 1.52
...
DB version: PostgreSQL 17.6 on x86_64-pc-linux-musl, compiled by gcc (Alpine 14.2.0) 14.2.0, 64-bit
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Projects that use the Spring Boot BOM

Many Spring Boot projects never set a Testcontainers version directly. In that case the testcontainers.version managed by Spring Boot decides. I checked the value in each spring-boot-dependencies POM on Maven Central.

Spring Boot Managed Testcontainers Docker 29
3.5.7, 3.5.8 1.21.3 Failing version
3.5.9 to 3.5.16 1.21.4 Passing version
4.0.0 2.0.2 Passing version
4.0.1 to 4.0.3 2.0.3 Passing version
4.0.4, 4.0.5 2.0.4 Passing version
4.0.6 to 4.0.9, 4.1.0 to 4.1.2 2.0.5 Passing version

I ran every Testcontainers version in the table (1.21.3, 1.21.4, 2.0.2 to 2.0.5) with the same test against the Docker 29.0.0 daemon. I didn’t start an actual Spring Boot project, so the right-hand column is the result for that Testcontainers version.

If you’re stuck on Spring Boot 3.5.7 or 3.5.8 with spring-boot-starter-parent, override just the <testcontainers.version> property with 1.21.4 in your pom.xml. The Spring Boot Maven Plugin documentation describes this way of changing an individual dependency version through a property. If you import spring-boot-dependencies instead of using the parent, the property has no effect. In that case, import the Testcontainers BOM before spring-boot-dependencies.

Workarounds when you can’t upgrade yet, and their risks

Sometimes you can’t bump a library right away. The approval process may be slow, or you may just need to build an old branch once. I verified two workarounds in the lab.

Client side: the api.version system property

The code in PR #11216 only applies the hardcoded default when the docker-java configuration has no API version. The docker-java docs say you can set api.version as a system property, in $HOME/.docker-java.properties, or in a docker-java.properties file on the classpath. I kept Testcontainers at 2.0.1 and passed only the property to Maven.

docker compose run --rm maven mvn -B -f tc2/pom.xml -Dtestcontainers.version=2.0.1 -Dapi.version=1.44 test
[main] INFO org.testcontainers.DockerClientFactory - Testcontainers version: 2.0.1
[main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy - Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
...
DB version: PostgreSQL 17.6 on x86_64-pc-linux-musl, compiled by gcc (Alpine 14.2.0) 14.2.0, 64-bit
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 18.44 s -- in com.example.tcapi.PostgresContainerTest

The -Dapi.version=1.44 on the command line reached the test JVM, and the test passed. I only verified the Maven command-line approach. If you run tests straight from your IDE, the docker-java docs describe putting api.version=1.44 in $HOME/.docker-java.properties. Every machine needs its own copy of that file, so it’s awkward to roll out to a whole team. Either way, it’s a stopgap until you upgrade.

Daemon side: the DOCKER_MIN_API_VERSION environment variable

The description of moby/moby#51186 says that starting the daemon with the DOCKER_MIN_API_VERSION environment variable restores an older minimum. In the lab, I set it on the dind daemon with a Compose override file.

# Workaround only: lower the daemon minimum API version again
services:
  docker:
    environment:
      DOCKER_MIN_API_VERSION: "1.24"

After restarting the daemon with this override, docker version --format {{.Server.MinAPIVersion}} prints 1.24. Testcontainers 2.0.1 then passed with BUILD SUCCESS. When I recreated the daemon without the override, the same command printed 1.44 again.

I still don’t recommend this. The same PR description calls API versions below 1.44 "best effort" and says the compatibility code may be trimmed later. Changing one daemon also affects every client that talks to it. And you’d have to change the Docker setup on every teammate’s machine and every CI runner.

Approach What you change Blast radius Verdict
Testcontainers 2.0.2+ / 1.21.4+ One line in pom.xml That project The real fix
-Dapi.version=1.44 Build command, CI script That build only Temporary, until you upgrade
DOCKER_MIN_API_VERSION Docker daemon Every client of that daemon Last resort

Summary

  • Docker 29.0.0 raised the daemon’s minimum API version to 1.44. Testcontainers 2.0.1 and 1.21.3 request 1.32, so they get a BadRequestException
  • The final exception is Could not find a valid Docker environment, which looks like Docker is down. The real cause is client version 1.32 is too old on the strategy line above it
  • On Docker Desktop, it shows up as empty JSON like Status 400: {"ID":"",...}. Since 29.3.0 the minimum is 1.40, but 1.32 is still rejected
  • The fix is Testcontainers 2.0.2 or later (1.21.4 or later on 1.x). Spring Boot 3.5.9 to 3.5.16 and 4.0.0 to 4.1.2 (the range I checked in the POMs) manage a fixed version
  • -Dapi.version=1.44 and DOCKER_MIN_API_VERSION are temporary workarounds until you upgrade

References

Leave a Comment