In Testcontainers, why does PostgreSQLContainer reject an image named registry.corp/pg:16?
answer
- The module knows one image well
- What does it assume about it?
- Name check fails fast, not slow
- Declare the substitution explicitly
- Your promise, not a verification
basics
~10 sModule containers assert that the image name matches the one they were written for. Wrap a mirrored image with DockerImageName.parse(name).asCompatibleSubstituteFor("postgres") to declare it behaves like upstream, and construction succeeds.
solid answer
~50 sA module container is not a generic wrapper: it bakes in assumptions about the image — which environment variables configure the database, which port it listens on, what the readiness log line looks like, where init scripts are picked up. To stop those assumptions being applied silently to an unrelated image, the constructor checks the image's name against the repository the module was written for and throws immediately if it does not match. A mirrored or hardened build under `registry.corp/pg` fails that check even though it is a Postgres. The fix is to state the compatibility explicitly: `new PostgreSQLContainer<>(DockerImageName.parse("registry.corp/pg:16").asCompatibleSubstituteFor("postgres"))`. That call is an assertion by you, not a verification by the library — if the substitute does not actually behave like upstream, you have simply moved the failure from a clear constructor error to a confusing wait-strategy timeout.
code
java · 5 linesDockerImageName image = DockerImageName
.parse("registry.corp/pg:16")
.asCompatibleSubstituteFor("postgres");
PostgreSQLContainer<?> db = new PostgreSQLContainer<>(image);go deeper
Recall that a module container only accepts its expected image name, and that a mirrored image is passed via DockerImageName.parse(...).asCompatibleSubstituteFor(...).
Explain what the module bakes in — env vars, port, wait strategy, init directory — and why a name check turns a slow timeout into a fast error.
Judge whether a hardened or mirrored variant is genuinely drop-in, and fall back to GenericContainer when the module's assumptions no longer hold.
Own image provenance for tests: which registry the org's suites pull from and how compatibility declarations are made once centrally rather than per test.
## What a module container assumes `PostgreSQLContainer`, `KafkaContainer` and their siblings exist because configuring those services by hand is fiddly. Each module encodes knowledge about a specific upstream image: the environment variables that set user, password and database; the default port; a wait strategy tuned to the messages that image logs when it is ready; the directory an entrypoint scans for initialisation scripts; how to build a JDBC URL. All of that is valid for the image the module targets and potentially wrong for anything else. ## Why the name check exists So the module asserts on construction that the image name matches the expected repository, and throws immediately when it does not. Without the check, passing an arbitrary image would produce a container that starts, ignores the environment variables the module set, never logs the expected line, and eventually fails the wait strategy with a timeout — a slow, confusing failure far from its cause. The name check converts that into a fast, explicit error with an actionable message. ## Declaring a substitute `DockerImageName.parse("registry.corp/pg:16").asCompatibleSubstituteFor("postgres")` marks the parsed name as a stand-in for the upstream repository, and the module then accepts it. This is common and legitimate: organisations mirror images into an internal registry, rebuild them on a hardened base, or use a drop-in variant. Read the call for what it is — a promise you make. Testcontainers does not inspect the image to verify the claim; it takes your word and proceeds to apply the module's assumptions. So the substitute must genuinely behave like upstream: same configuration variables, same port, same readiness signal, same init-script directory. A hardened rebuild that strips the entrypoint's init-script handling will pass construction and then quietly not run your seed script. ## When the substitute is not really compatible If a variant differs in the ways the module cares about, do not force it through. Use `GenericContainer` and configure the environment, ports and wait strategy yourself. You lose the module's convenience methods and gain a container that actually matches the image you are running. ## Doing it once instead of everywhere Calling `asCompatibleSubstituteFor` at each construction site is fine for a handful of tests and unmanageable across a large codebase; the same substitution then belongs in one place rather than in every test. That is a separate mechanism, but the compatibility declaration is the building block it relies on. ## Version note `DockerImageName` with `parse` and `asCompatibleSubstituteFor` is the modern API; older code sometimes passed a bare `String`, and the string constructors are the ones that give you the compatibility error today. ## Interview framing The good answer explains *why* the check exists — the module's baked-in assumptions — before giving the API call. Reciting `asCompatibleSubstituteFor` alone reads as memorised; adding "and it is my assertion, so the image had better behave like upstream" reads as experience.
- What breaks if you declare compatibility for an image that is not really compatible?The constructor stops complaining and the module applies assumptions the image does not honour. Typically the environment variables are ignored so credentials differ, or the readiness log line never appears and the wait strategy times out after a minute with no useful message. You have traded a clear error for a confusing one — which is exactly what the name check existed to prevent.
- When should you use GenericContainer instead of forcing a module container to accept your image?When the variant genuinely differs in the ways the module cares about: different configuration variables, a different port, no init-script directory, a different readiness signal. Configure environment, ports and a wait strategy yourself. You lose helpers like getJdbcUrl(), but the container's configuration then matches the image you are actually running.
- Why does the compatibility check happen at construction rather than at start?Because it is a static mismatch between what you asked for and what the module supports, and failing before any Docker work happens gives the fastest, clearest signal. Deferring it to start would mix an obvious configuration error into pulls, container creation and wait-strategy timeouts, where the real cause is much harder to see.
saying these in an interview costs you the question
- Thinks the check verifies the image really is Postgres
- Calls asCompatibleSubstituteFor on an image that behaves differently
- Believes only Docker Hub images are allowed
- Tries to bypass the check by changing the pull policy
- Assumes a hardened rebuild is always drop-in compatible