testcontainers/testcontainers-java · error · java.lang.RuntimeException

Can't close DockerClient

Error message

Can't close DockerClient

What it means

ImageFromDockerfile.resolve() builds an image from a Dockerfile and must close the temporary DockerClient in a finally-ish path; if closing the client throws IOException it is rethrown as RuntimeException("Can't close DockerClient"). It usually masks an I/O problem on the underlying docker client transport/streams.

Solutions

  1. Look at the cause (IOException) and earlier logs — the real failure is usually the image build itself, not the close
  2. Ensure the Docker daemon stayed up during the build; retry the build
  3. Check ulimit/file-descriptor limits if many containers/images are built in one JVM
  4. Upgrade testcontainers/docker-java to pick up transport close-handling fixes
  5. Catch and log this in test scaffolding so the original build failure isn't hidden

Example fix

// before
String id = new ImageFromDockerfile().withDockerfileFromBuilder(b -> b.build()).get(); // masked IOException
// after
// fix the underlying build failure (e.g. invalid Dockerfile step) so close() succeeds
String id = new ImageFromDockerfile()
    .withDockerfileFromBuilder(b -> b.from("alpine:3.19").run("echo hi").build())
    .get();
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure daemon is healthy before a long build
new ProcessBuilder("docker","info").inheritIO().start().waitFor() == 0;

Type guard

null

Try / catch

try {
    String id = imageFromDockerfile.get();
} catch (RuntimeException e) {
    if (e.getMessage() != null && e.getMessage().contains("Can't close DockerClient")) {
        // the close-time IOException usually masks an earlier build/transport failure
        throw new IllegalStateException("Image build failed; see cause: " + e.getCause(), e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling imageId()/resolve() on an ImageFromDockerfile and the docker-java client's close() raises IOException — e.g. the transport/connection to the daemon is already broken after the build or awaitImageId() failed leaving streams in a bad state.

Common situations: Docker daemon dropped the connection during a long image build; build failed earlier causing streams to be closed twice; resource exhaustion (too many open files); abrupt daemon restart.

Related errors


AI-assisted analysis of testcontainers/testcontainers-java@8e549514e3 (2026-09-12). Data as JSON: /api/errors/a5e71f4da3491af9. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/org/testcontainers/images/builder/ImageFromDockerfile.java:169

                    transferable.transferTo(tarArchive, destination);
                    bytesToDockerDaemon += transferable.getSize();
                }
                tarArchive.finish();
            }

            log.info("Transferred {} to Docker daemon", FileUtils.byteCountToDisplaySize(bytesToDockerDaemon));
            if (bytesToDockerDaemon > FileUtils.ONE_MB * 50) {
                log.warn( // warn if >50MB sent to docker daemon
                    "A large amount of data was sent to the Docker daemon ({}). Consider using a .dockerignore file for better performance.",
                    FileUtils.byteCountToDisplaySize(bytesToDockerDaemon)
                );
            }

            exec.awaitImageId();

            return dockerImageName;
        } catch (IOException e) {
            throw new RuntimeException("Can't close DockerClient", e);
        }
    }

    protected void configure(BuildImageCmd buildImageCmd) {
        buildImageCmd.withTags(Collections.singleton(getDockerImageName()));
        this.dockerFilePath.ifPresent(buildImageCmd::withDockerfilePath);
        this.dockerfile.ifPresent(p -> {
                buildImageCmd.withDockerfile(p.toFile());
                dependencyImageNames = new ParsedDockerfile(p).getDependencyImageNames();

                if (dependencyImageNames.size() > 0) {
                    // if we'll be pre-pulling images, disable the built-in pull as it is not necessary and will fail for
                    // authenticated registries
                    buildImageCmd.withPull(false);
                }
            });

        this.buildArgs.forEach(buildImageCmd::withBuildArg);

View on GitHub (pinned to 8e549514e3)