[Bug]: Compose `!override` tag is treated like `!reset`, so the wrong images are pre-pulled

Author: srivathsav01Created Sep 29, 2026Updated Sep 29, 2026
Labelstype/bug

Module

Core

Testcontainers version

2.0.5

Using the latest Testcontainers version?

Yes

Host OS

Windows 10

Host Arch

x86_64

Docker version

bash
N/A. The problem is in how Testcontainers parses the compose files before anything is sent to Docker, so it reproduces without a Docker daemon (see the reproduction below).

What happened?

Before running docker compose up, Testcontainers reads the compose files itself, collects every image the services use (including base images of Dockerfiles they build), and pulls them with its own Docker client (ComposeDelegate#pullImages). According to the comment in pullImages, this is so that images from registries that need credential helpers can be pulled even when Compose can't use those credentials: originally because of docker/compose#5854 (docker-compose build ignored credential helpers), and still when Compose itself runs in a container. When Compose then starts, the images are already present.

Support for the !override tag was added in #11490 (for #11489), but it is handled exactly like !reset. The tagged value is replaced with null:

java
if (node.getTag().equals(new Tag("!reset")) || node.getTag().equals(new Tag("!override"))) {
    return null;
}

In Compose, !reset removes a value, but !override means "use this value instead of the one from the previous files" (https://docs.docker.com/reference/compose-file/merge/#replace-value). Because the overriding value is dropped, the image from the base file is pre-pulled instead of the one Compose will actually run.

Overriding an Image

Image

Compose runs postgres:16, but Testcontainers pre-pulls postgres:15.

Overriding a whole scenario

Image

Compose runs postgres:16 and redis:7, but Testcontainers pre-pulls postgres:15 and redis:6, neither of which is used.

Here db becomes null, and ParsedDockerComposeFile#parseAndValidate then stops processing all remaining services, because the loop uses break instead of continue when a service definition is not a map:

java
if (!(serviceDefinition instanceof Map)) {
    log.debug("Compose file {} has an unknown format: service '{}' is not Map", composeFileName, serviceName);
    break;
}

Impact : The images that are actually needed are not pre-pulled, so Compose has to pull them itself. With a local Compose this usually just works. When Compose runs in a container (e.g. new ComposeContainer(DockerImageName.parse("docker:25.0.5"), ...)) and the overriding image is in a private registry, it can fail, because the containerised Compose has no access to the host's credential helpers, even though the user is logged in.

Relevant log output

bash
Reproduction using the public `DockerComposeFiles#getDependencyImages()` (the set `pullImages()` iterates over) against testcontainers 2.0.5:

1) image replaced with !override
   pre-pulled: [postgres:15]            expected: [postgres:16]
2) whole service replaced with !override
   pre-pulled: [postgres:15]            expected: [postgres:16]
3) services after a !override service
   pre-pulled: [redis:6, postgres:15]   expected: [postgres:16, redis:7]

Additional Information

No response

Source: testcontainers/testcontainers-java