Module
Core
Testcontainers version
2.0.5
Using the latest Testcontainers version?
Yes
Host OS
Windows 10
Host Arch
x86_64
Docker version
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:
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
Compose runs postgres:16, but Testcontainers pre-pulls postgres:15.
Overriding a whole scenario
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:
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
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
Module
Core
Testcontainers version
2.0.5
Using the latest Testcontainers version?
Yes
Host OS
Windows 10
Host Arch
x86_64
Docker version
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 inpullImages, 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 buildignored credential helpers), and still when Compose itself runs in a container. When Compose then starts, the images are already present.Support for the
!overridetag was added in #11490 (for #11489), but it is handled exactly like!reset. The tagged value is replaced withnull:In Compose,
!resetremoves a value, but!overridemeans "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
Compose runs
postgres:16, but Testcontainers pre-pullspostgres:15.Overriding a whole scenario
Compose runs
postgres:16andredis:7, but Testcontainers pre-pullspostgres:15andredis:6, neither of which is used.Here
dbbecomesnull, andParsedDockerComposeFile#parseAndValidatethen stops processing all remaining services, because the loop usesbreakinstead ofcontinuewhen a service definition is not a map: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
Additional Information
No response