skip to content

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