skip to content

Networking and Wait Strategies

You will learn why Testcontainers maps container ports to random host ports and how getMappedPort() and shared Networks with aliases let tests and containers find each other, plus the wait strategies (port, log, HTTP, healthcheck) that define 'ready'. Interviewers dig here because flaky integration tests are usually a readiness or networking bug, and diagnosing 'it passes locally but races in CI' is exactly what they want to hear you reason through.

on this pageshow

questions

4

In Testcontainers, why must a test use getHost() and getMappedPort() instead of a fixed port?

level: juniorimportance: must knowfreq 72%

answer

  1. Docker publishes on an ephemeral host port
  2. Number is unknown until the container starts
  3. Avoids collisions and enables parallel tests
  4. Not always localhost either
  5. getMappedPort takes the container-side port

basics

~20 s

Testcontainers publishes each exposed container port on a random free port on the Docker host, so the number differs every run and cannot be hardcoded. getHost() matters too, because the daemon is not always on localhost.

solid answer

~50 s

When you declare `withExposedPorts(6379)`, Testcontainers asks Docker to publish that container port on an *ephemeral* host port chosen at start time. Random binding is deliberate: it means two containers of the same image, two builds on one CI agent, and parallel test classes never collide on a fixed port. The consequence is that the number is unknown until the container is running — `getMappedPort(6379)` before `start()` throws `IllegalStateException`, and the value differs on every run. `getHost()` is the matching call for the address: it usually returns localhost, but returns the daemon's host when `DOCKER_HOST` points at a TCP or remote daemon, or when you run against Testcontainers Cloud. So the rule is to build every connection string from both accessors after start, never from a literal. `getFirstMappedPort()` is shorthand when the container exposes exactly one port.

code

java · 8 lines
java
GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

redis.start();

String host = redis.getHost();             // not necessarily "localhost"
Integer port = redis.getMappedPort(6379);  // random free host port, differs per run
String uri = "redis://" + host + ":" + port;

go deeper

for a junior

Recall the pair of calls and the order: start the container, then build the connection string from getHost() and getMappedPort(exposedPort). Never write a fixed host port or the word localhost.

for a middle

Explain why the binding is random — collision avoidance and parallel execution — and what getMappedPort() maps from and to. Know that it throws before the container starts.

for a senior

Point out where eager configuration breaks this, and why getHost() is the portability lever when builds run against remote daemons or a Docker host that is not the test machine.

for a principal

Frame it as the isolation property that makes container-backed suites parallelisable at all, and set the team rule that connection coordinates are always derived at runtime, never configured.

## What Docker is actually doing A container has its own network namespace. A process listening on port 6379 *inside* the container is not reachable from the host until that port is published — Docker sets up a mapping from some host port to the container port. With `docker run -p 6379:6379` you pick the host port yourself; with `-p 6379` (no host side) Docker picks a free ephemeral port. Testcontainers always does the latter. `withExposedPorts(6379)` means "publish container port 6379 on whatever host port is free", and the chosen number is discoverable only after the container starts. ## Why random rather than fixed Fixed host ports are a source of exactly the failures integration suites cannot afford: - **Collisions with local services.** A developer running Postgres on 5432 cannot run a test that binds 5432. - **Collisions between tests.** Two test classes each wanting their own database instance would fight over one number. - **Collisions on shared CI agents.** Several jobs on one machine, each starting the same image, would serialise or fail. - **Parallelism.** Random ports are what make it safe to run container-backed tests concurrently at all. The cost is that the port becomes runtime data, and the API forces you to treat it that way. ## The two accessors `getMappedPort(int containerPort)` takes the *container-side* port you exposed and returns the host-side port Docker chose. Calling it before the container is started throws `IllegalStateException` — the mapping does not exist yet. Calling it with a port you never exposed is likewise an error, because nothing was published. `getFirstMappedPort()` returns the host port for the first exposed port; convenient for single-port images, ambiguous the moment there are two, so prefer the explicit form when a container exposes several. `getHost()` returns the host name or address at which those mapped ports are reachable. On a plain local Docker installation this is localhost. It is *not* localhost when `DOCKER_HOST` points at a remote daemon over TCP, when you are using Testcontainers Cloud, or in some rootless and VM-backed setups. Writing "localhost" into a URL is therefore a portability bug that only appears on someone else's machine — usually the build agent's. ## The mirror image: inside the network The mapped port is a *host-side* concept. Traffic that never leaves Docker's network — one container calling another over a user-defined network — uses the container's own internal port and never the mapped one. Mixing these up is the single most common Testcontainers networking mistake, and it produces connection-refused errors that look mysterious because the mapped port genuinely works from the test JVM. ## Where the accessors hide The preconfigured module containers wrap this for you: a database module builds a JDBC URL from the same accessors internally, so you call one method instead of assembling a string. That convenience does not change the model — underneath, it is still `getHost()` plus a mapped port resolved after startup. ## Ordering pitfalls Because the values exist only after start, anything that captures configuration *early* must be given a supplier or must run after startup. Static initialisers that build a URL before the container has been started, constructor-time field initialisation, and configuration read at class-load time are the usual offenders. The fix is always the same: resolve the coordinates after `start()` and inject them lazily rather than eagerly. ## What good answers include A strong answer names both accessors, explains random binding as an isolation/parallelism decision rather than an arbitrary quirk, mentions that `getHost()` is not always localhost, and notes that the values are only valid after the container is running. A weak answer treats `getMappedPort()` as boilerplate to copy without knowing what it returns.

  • When is getHost() not localhost?
    When the Docker daemon is not local to the test JVM: `DOCKER_HOST` pointing at a TCP or remote daemon, Testcontainers Cloud, and some rootless or VM-backed setups. That is exactly why the accessor exists — hardcoding localhost passes on a laptop and fails on the build agent.
  • What happens if you call getMappedPort() before start()?
    It throws `IllegalStateException`. The mapping is created by Docker when the container starts, so there is nothing to return beforehand. It is the classic failure in static initialisers that build a connection URL eagerly.
  • Does a container talking to another container use the mapped port?
    No. Mapped ports are host-side only. Container-to-container traffic on a shared Docker network addresses the target by its network alias and its own internal port, because that traffic never traverses the host's published mapping.

saying these in an interview costs you the question

  • Hardcodes the container port as the host port
  • Writes localhost into the connection URL
  • Builds the URL in a static initialiser before start()
  • Thinks the mapped port is stable across runs
  • Uses the mapped port for container-to-container calls

context

open as a page

In Testcontainers, how do two containers reach each other, and which port do they use?

level: middleimportance: should knowfreq 46%

basics

~20 s

Put both containers on one Testcontainers Network and give the target a network alias; the other container connects to that alias on the target's own internal port. Mapped ports are host-side only and must not be used between containers.

open as a page

In Testcontainers, when do you pick Wait.forLogMessage over Wait.forHttp or Wait.forHealthcheck?

level: middleimportance: should knowfreq 52%

basics

~20 s

Pick by the readiness signal the image actually emits: a log line for servers that announce readiness in stdout, an HTTP probe for services with a health endpoint, and Wait.forHealthcheck only when the image itself declares a Docker HEALTHCHECK.

open as a page

A Testcontainers test times out waiting for its container only on CI. How do you diagnose it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Get the container's own logs first, then separate the three causes: the container crashed, the wait strategy never matches what the image emits, or startup is genuinely slower on the agent. Only the last one is fixed by raising the startup timeout.

open as a page