Redpanda¶
dev.rightsize.modules.RedpandaContainer — a single-node Redpanda broker
(Kafka-API-compatible) with its schema registry enabled.
Defaults¶
| Default image | redpandadata/redpanda:v24.2.4 |
| Exposed ports | 9092 (Kafka API), 9093 (internal listener), 8081 (schema registry) |
| Wait strategy | Wait.forLogMessage(".*Successfully started Redpanda.*") |
Helpers¶
| Member | Returns |
|---|---|
bootstrapServers: String |
The PLAINTEXT:// bootstrap-servers address for the running broker (EXTERNAL listener, host-reachable) |
schemaRegistryUrl: String |
The schema registry's base URI |
Example¶
package dev.rightsize.modules
import dev.rightsize.modules.RedpandaContainer
import org.apache.kafka.clients.admin.AdminClient
import org.apache.kafka.clients.admin.NewTopic
import org.apache.kafka.clients.consumer.KafkaConsumer
import org.apache.kafka.clients.producer.KafkaProducer
import org.apache.kafka.clients.producer.ProducerRecord
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.Duration
import java.util.Properties
class RedpandaContainerTest {
@Test
fun `produce then consume a record`() {
val redpanda = RedpandaContainer()
redpanda.start()
try {
val common = Properties().apply { put("bootstrap.servers", redpanda.bootstrapServers) }
AdminClient.create(common).use {
it.createTopics(listOf(NewTopic("t1", 1, 1))).all().get()
}
val producerProps = Properties().apply {
putAll(common)
put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer")
}
KafkaProducer<String, String>(producerProps).use {
it.send(ProducerRecord("t1", "k", "v")).get()
}
val consumerProps = Properties().apply {
putAll(common)
put("group.id", "g1")
put("auto.offset.reset", "earliest")
put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
}
KafkaConsumer<String, String>(consumerProps).use { consumer ->
consumer.subscribe(listOf("t1"))
val records = consumer.poll(Duration.ofSeconds(15))
assertEquals("v", records.first().value())
}
} finally {
redpanda.stop()
}
}
}
Backend notes: the dual-listener trick¶
Redpanda (and Kafka) both advertise a listener address to clients as part of their own protocol handshake — a producer or consumer doesn't just connect to the bootstrap address you give it, it also trusts whatever address the broker advertises back for subsequent connections. That advertised address has to be one the host JVM can actually reach — but the real host port isn't known until after rightsize has allocated it, which happens moments before boot.
This module solves it by overriding customizeSpec — a GenericContainer extension
point that gets a callback resolving "guest port → its just-allocated host port" the
instant before the container is created — and uses it to launch Redpanda with two
listeners:
- EXTERNAL advertises
127.0.0.1:<the real allocated host port>— this is what your host-sideKafkaProducer/KafkaConsumeractually dials. - INTERNAL advertises a fixed alias (
redpanda:9093) for sibling containers on the sameNetworkto resolve — native Docker networking on the Docker backend, best-effort via the exec-tunnel alias emulation on microsandbox.
You don't need to configure any of this yourself — RedpandaContainer() with no
arguments already does it — but if you're writing your own GenericContainer subclass
for a broker with the same "advertises its own address" behavior, customizeSpec is
the extension point to reach for; see the class's own KDoc
(RedpandaContainer.customizeSpec) for the exact command line it constructs.
Registry availability is worth knowing about separately: the module pulls from
Docker Hub (redpandadata/redpanda) — Redpanda's own docker.redpanda.com proxy
serves identical images but rate-limits anonymous pulls aggressively enough to block
CI runs. If you hit persistent pull failures, seed the image into your local msb cache
ahead of time (docker save <img> -o /tmp/img.tar && msb load -i /tmp/img.tar -t <img>) rather than retrying the pull
indefinitely.