skip to content

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

level: middleimportance: should knowfreq 46%

answer

  1. One user-defined network for both containers
  2. Docker's embedded DNS resolves the name
  3. Give the target a stable alias
  4. Mapped ports are for the host only
  5. Peer connects on the real listening port

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.

solid answer

~40 s

Create a network with `Network.newNetwork()` (or use the shared `Network.SHARED`), attach each container with `withNetwork(network)`, and label the target with `withNetworkAliases("db")`. Docker's embedded DNS on a user-defined network resolves that alias to the target container's address, so the client container connects to `db:5432` — the port the server actually listens on inside the container. `getMappedPort()` is wrong here: port publishing exists so the *host* can reach the container, and container-to-container traffic never traverses it. The test JVM is on the host, so it keeps using `getHost()` and `getMappedPort()` for its own connections; the same target therefore has two different addresses depending on who is calling. For the reverse direction — a container calling a service running on the host — Testcontainers offers `Testcontainers.exposeHostPorts(port)`, reachable inside containers as `host.testcontainers.internal`.

code

java · 17 lines
java
Network network = Network.newNetwork();

PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16-alpine")
        .withNetwork(network)
        .withNetworkAliases("db");

GenericContainer<?> app = new GenericContainer<>("my/app:latest")
        .withNetwork(network)
        .withExposedPorts(8080)
        // internal port of the database container, NOT getMappedPort(5432)
        .withEnv("DB_URL", "jdbc:postgresql://db:5432/test");

db.start();
app.start();

// the test JVM lives on the host, so it uses the host-side coordinates
String appUrl = "http://" + app.getHost() + ":" + app.getMappedPort(8080);

go deeper

for a junior

Know the shape: Network.newNetwork(), withNetwork on both containers, withNetworkAliases on the one being called, and the peer connects to that alias on the normal service port.

for a middle

Explain why the mapped port is wrong between containers — publishing is a host-side mapping — and that Docker's embedded DNS on a user-defined network is what resolves the alias.

for a senior

Handle the full topology: the same container addressed two ways at once, ordering and readiness between dependent containers, and the container-to-host direction via exposeHostPorts and host.testcontainers.internal.

for a principal

Decide how far a test suite should reproduce production topology in Docker versus stubbing the dependency, and what that costs in runtime, CI resources and diagnosis difficulty when it breaks.

## Three directions of traffic A container-backed test has up to three distinct paths, and each has its own addressing rule: 1. **Test JVM (host) to container** — use `getHost()` and `getMappedPort(containerPort)`. 2. **Container to container** — use a network alias and the target's *internal* port. 3. **Container to a service on the host** — expose the host port explicitly and address `host.testcontainers.internal`. Most confusion in this area is a case of applying rule 1 where rule 2 belongs. ## Setting up a shared network `Network.newNetwork()` creates a fresh user-defined Docker bridge network with a generated name; it implements `AutoCloseable` and Testcontainers cleans it up with the rest of the test resources. Every container that should see the others gets `withNetwork(network)`. On a user-defined bridge network — as opposed to Docker's legacy default bridge — Docker runs an embedded DNS server that resolves container names and network aliases to container IPs. `withNetworkAliases("db")` gives the target a stable, predictable name on that network, which matters because container names are otherwise generated. A container may carry several aliases. `Network.SHARED` is a convenience constant: a single network instance shared across the JVM, useful when many containers in a suite need to see each other without threading a `Network` reference through the code. ## Why the internal port, not the mapped port Port publishing (`-p`) creates a mapping on the *host's* network stack: traffic arriving at the host's ephemeral port is forwarded into the container. Two containers on the same Docker network talk directly over that network; their packets never reach the host mapping. So the client must address the port the server process actually binds — 5432 for Postgres, 9092 for a broker, 8080 for a web service — not the number `getMappedPort()` reports. A useful mental check: if the connection string is being consumed *inside* a container, no accessor from the Testcontainers API should appear in it except the alias you chose yourself. ## Consequences for configuration When a container under test needs the address of a dependency, you pass it as environment configuration built from the alias, for example an environment variable holding `jdbc:postgresql://db:5432/test`. When the *test* needs the same database, it builds a different string from `getHost()` and `getMappedPort(5432)`. Both are correct simultaneously; they are two views of one container. This is also why exposing a port is still worth doing on a container that is only consumed by another container: you expose it so the test can look inside — assert on data, inspect state — not because the peer container needs it. ## Startup ordering Attaching to a network does not order startup. If the client container must not start before the dependency is ready, start the dependency first (or declare a dependency with `dependsOn`) *and* give it a wait strategy that reflects real readiness. On a shared network, a client that starts too early sees DNS resolve but the connection refused. ## Reaching the host from a container The reverse direction comes up when the system under test runs in the JVM (a Spring Boot app on a random port) and a container must call back into it — a webhook receiver, a callback URL, a mock consumer. `org.testcontainers.Testcontainers.exposeHostPorts(int...)` makes the given host ports reachable from containers at the special host name `host.testcontainers.internal`. Call it before starting the containers that need it. ## Cleanup Networks, like containers, are labelled by Testcontainers and removed by the Ryuk resource reaper when the JVM exits, so a crashed run does not leave orphan networks behind. A `Network` created per test class is inexpensive; sharing one across a suite is mostly a convenience choice. ## What interviewers listen for The distinction between host-side and network-side addressing, stated without hedging, plus the observation that the same container legitimately has two addresses. Candidates who have only used single-container tests usually have not met this; candidates who have wired an app container to a database container answer it immediately.

  • Can a container reach a service running in the test JVM on the host?
    Yes — call `Testcontainers.exposeHostPorts(port)` before starting the containers, and inside them the service is reachable at the host name `host.testcontainers.internal` on that same port. It is the usual way to let a container deliver a webhook back to the application under test.
  • What is Network.SHARED for?
    It is a single shared network instance for the JVM, so containers across a suite can see each other without passing a `Network` object around. Use it when nearly everything needs one network; prefer an explicit `Network.newNetwork()` when you want isolation between test classes.
  • Does putting two containers on one network guarantee the dependency is up first?
    No. Network membership says nothing about ordering or readiness. Start the dependency first and give it a wait strategy that reflects genuine readiness; otherwise the client container can start, resolve the alias by DNS, and still be refused on connect.

saying these in an interview costs you the question

  • Passes getMappedPort() into another container's config
  • Uses localhost between two containers
  • Assumes the default bridge network resolves names
  • Thinks network membership orders container startup
  • Expects the container to reach the host as localhost

context