Skip to content

[Bug]: RedpandaContainer fails to start in testcontainers 2.x — getMappedPort() unavailable at containerIsCreated time #11781

@rhanton

Description

@rhanton

Module

Redpanda

Testcontainers version

2.0.5

Using the latest Testcontainers version?

Yes

Host OS

OSX

Host Arch

ARM

Docker version

Client:
 Version:           29.1.4-rd
 API version:       1.52
 Go version:        go1.25.5
 Git commit:        3c6914c
 Built:             Fri Jan  9 20:45:44 2026
 OS/Arch:           darwin/arm64
 Context:           default

Server:
 Engine:
  Version:          29.1.3
  API version:      1.52 (minimum version 1.41)
  Go version:       go1.25.5
  Git commit:       fbf3ed25f893e6ce21336f1101590e40a13934f4
  Built:            Sun Dec 14 05:31:58 2025
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          v2.2.0
  GitCommit:        1c4457e00facac03ce1d75f7b6777a7a851e5c41
 runc:
  Version:          1.4.0
  GitCommit:        8bd78a9977e604c4d5f67a7415d7b8b8c109cdc4
 docker-init:
  Version:          0.19.0
  GitCommit:

What happened?

RedpandaContainer throws java.lang.IllegalArgumentException: Requested port (9092) is not mapped when starting under testcontainers 2.x, making the container completely unusable.

Steps to Reproduce:

@Container
static RedpandaContainer redpanda = new RedpandaContainer(
    "docker.redpanda.com/redpandadata/redpanda:v26.1.9");

Running any test that uses this container produces:

Caused by: java.lang.IllegalArgumentException: Requested port (9092) is not mapped
    at org.testcontainers.containers.ContainerState.getMappedPort(ContainerState.java:175)
    at org.testcontainers.redpanda.RedpandaContainer.containerIsCreated(RedpandaContainer.java:...)

Root Cause

RedpandaContainer.containerIsCreated() calls getMappedPort(9092) to write the correct --advertise-kafka-addr into the startup script before the container process begins. In testcontainers 1.x, Docker's random host port assignment happened at container creation time, so getMappedPort() was already valid inside containerIsCreated.
In testcontainers 2.x, the port assignment lifecycle changed: the ephemeral host port is only resolved after the container starts, not when it is created. containerIsCreated now fires too early — the port binding hasn't been populated yet — so getMappedPort() throws.

Proposed Fix

Replace the getMappedPort() call in containerIsCreated with a pre-allocated port strategy, similar to how some other testcontainers modules handle this:

At construction time, open a ServerSocket(0) to let the OS assign a free port, record that port number, then close the socket.
Use withCreateContainerCmdModifier to bind that specific host port to container port 9092 (instead of relying on Docker's random assignment).

Bake the pre-allocated port directly into the startup script / --advertise-kafka-addr flag — no lifecycle hook or getMappedPort() call required at all.

Return the pre-allocated port from getBootstrapServers().
Sketch of the change inside RedpandaContainer:

// Construction time — before any container lifecycle hooks run
private final int hostPort = findFreePort();

private static int findFreePort() {
    try (ServerSocket s = new ServerSocket(0)) {
        s.setReuseAddress(true);
        return s.getLocalPort();
    } catch (IOException e) {
        throw new RuntimeException("Could not allocate a free port for Redpanda", e);
    }
}

public RedpandaContainer(DockerImageName imageName) {
    super(imageName);
    withCreateContainerCmdModifier(cmd ->
        cmd.withHostConfig(cmd.getHostConfig()
            .withPortBindings(new PortBinding(
                Ports.Binding.bindPort(hostPort),
                ExposedPort.tcp(KAFKA_PORT)))));
    withCommand(
        "redpanda", "start",
        "--kafka-addr",           "PLAINTEXT://0.0.0.0:" + KAFKA_PORT,
        "--advertise-kafka-addr", "PLAINTEXT://localhost:" + hostPort,
        ...
    );
}

// containerIsCreated() can be removed entirely — it's no longer needed.

@Override
public String getBootstrapServers() {
    return "localhost:" + hostPort;
}

Trade-off:

There is a small TOCTOU window between closing the ServerSocket and Docker binding the port. In practice this is negligible on development/CI machines, and it is the same pattern already used by other testcontainers modules (e.g. KafkaContainer in some configurations).

Relevant log output

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions