Problem
Image builds go through POST /build on the Docker Engine API (DockerImageOperations.BuildAsync), which uses the legacy builder. Any Dockerfile that depends on BuildKit cannot be built with Testcontainers, even when it builds fine with docker build on the same host. This is documented as a limitation in docs/api/create_docker_image.md and has produced several issues:
The position so far (#1193, discussioncomment-10315903) is that BuildKit is not part of the Docker Engine API, and that support would be added as soon as it is possible or someone comes up with a workaround. This is a proposal for such a workaround, using the same approach the new Compose support already uses.
Solution
Opt-in BuildKit builds run by the Docker CLI inside a container, following the pattern from Compose support (#1750):
- A keep-alive
docker:28-cli container. That image ships the buildx plugin. Started the way ComposeBuilder starts its container (src/Testcontainers/Builders/ComposeBuilder.cs:386-387).
- The Docker socket bind-mounted read-only through the existing
UnixSocketMount (ComposeBuilder.cs:374), so the build runs against the same daemon as the rest of the test session.
- The build context copied into the container with resource mapping, the same way compose files are (
ComposeBuilder.cs:377). The tarball that ITarArchive.Tar already produces can be reused here.
docker buildx build --load invoked with ExecAsync, the same shape as ComposeContainer.ComposeUpCommand (src/Testcontainers/Containers/ComposeContainer.cs:51,409).
With --load the result is written to the daemon's image store, so everything after the build behaves as it does today: image name resolution, WithImage, and resource reaper labels passed as --label. The existing IImageFromDockerfileConfiguration maps onto CLI flags directly: Dockerfile to --file, Image to --tag, BuildArguments to --build-arg, Labels to --label, Target to --target, plus new options for --secret, --ssh and --platform.
The Engine API path stays the default and is not touched. BuildKit is enabled explicitly (something like WithBuildKit(), exact API up for discussion). A first PR would cover heredoc (#1247) and build secrets (#1406).
Verified locally
Docker 29.1.3, docker:28-cli with buildx v0.29.1, daemon BuildKit v0.26.2, socket mounted read-only, context copied in with docker cp, then docker buildx build --load inside the container. Results:
- The image is written to the host daemon's image store and is visible to
docker images outside the build container.
- Heredoc produces the expected file contents, and the entrypoint runs.
--secret id=mysecret,src=... is readable at /run/secrets/mysecret during RUN, and /run/secrets is not present in the final image.
- Both a
--label and a LABEL fed by --build-arg (the org.testcontainers.resource-reaper-session pattern from the docs) are set on the resulting image.
Questions before I write any code
- API shape: a flag on
ImageFromDockerfileBuilder, or a separate builder type? Should the CLI image be configurable and pinned the way ComposeBuilder(string image) is?
- Non-socket endpoints: TLS/TCP hosts and Windows named pipes cannot be bind-mounted. Fall back to the legacy builder with a warning, or throw?
- Secrets: which sources to support (file, environment variable, in-memory), and making sure secret values reach only the builder container, never the context tarball or the image.
- Builder lifecycle: one build container per build or a shared one, and whether buildx cache should be reused across builds at all in a test run.
- Longer term: would you rather aim for native BuildKit session support in
testcontainers/Docker.DotNet (POST /build?version=2&session=<id> plus the gRPC session for filesync, auth and secrets), with this as an interim step? Since the client is a Testcontainers-owned fork that route is not blocked upstream, it is just a lot more work.
Benefit
Alternatives
- Call the host
docker CLI. Simplest option, but it breaks the API-only design and depends on whatever CLI and buildx version the host happens to have. Not proposed.
- Native BuildKit session support in
Docker.DotNet. Best fit with the current architecture, but it needs BuildKit protobufs and a gRPC session over a hijacked connection in .NET, and it holds up both open issues until that is done.
- Leave it as is and document workarounds. Build the image with the CLI outside the test and pass the name in, or replace heredocs with
WithResourceMapping and WithStartupCallback. Both work, but they move build orchestration out of the test and neither helps with secrets.
- Use a
moby/buildkit container instead of docker:28-cli, so a real buildkitd rather than the daemon's builder. More control, but a heavier moving part and further from the Compose approach.
Would you like to help contributing this enhancement?
Yes
Problem
Image builds go through
POST /buildon the Docker Engine API (DockerImageOperations.BuildAsync), which uses the legacy builder. Any Dockerfile that depends on BuildKit cannot be built with Testcontainers, even when it builds fine withdocker buildon the same host. This is documented as a limitation indocs/api/create_docker_image.mdand has produced several issues:RUN cat <<EOF) silently produces an empty file, and the container then fails to start withexec format error--secret,RUN --mount=type=secret)$BUILDPLATFORM([Enhancement]: $BUILDPLATFORM #993), and anything else behind# syntax=docker/dockerfile:1.xThe position so far (#1193, discussioncomment-10315903) is that BuildKit is not part of the Docker Engine API, and that support would be added as soon as it is possible or someone comes up with a workaround. This is a proposal for such a workaround, using the same approach the new Compose support already uses.
Solution
Opt-in BuildKit builds run by the Docker CLI inside a container, following the pattern from Compose support (#1750):
docker:28-clicontainer. That image ships the buildx plugin. Started the wayComposeBuilderstarts its container (src/Testcontainers/Builders/ComposeBuilder.cs:386-387).UnixSocketMount(ComposeBuilder.cs:374), so the build runs against the same daemon as the rest of the test session.ComposeBuilder.cs:377). The tarball thatITarArchive.Taralready produces can be reused here.docker buildx build --loadinvoked withExecAsync, the same shape asComposeContainer.ComposeUpCommand(src/Testcontainers/Containers/ComposeContainer.cs:51,409).With
--loadthe result is written to the daemon's image store, so everything after the build behaves as it does today: image name resolution,WithImage, and resource reaper labels passed as--label. The existingIImageFromDockerfileConfigurationmaps onto CLI flags directly:Dockerfileto--file,Imageto--tag,BuildArgumentsto--build-arg,Labelsto--label,Targetto--target, plus new options for--secret,--sshand--platform.The Engine API path stays the default and is not touched. BuildKit is enabled explicitly (something like
WithBuildKit(), exact API up for discussion). A first PR would cover heredoc (#1247) and build secrets (#1406).Verified locally
Docker 29.1.3,
docker:28-cliwith buildx v0.29.1, daemon BuildKit v0.26.2, socket mounted read-only, context copied in withdocker cp, thendocker buildx build --loadinside the container. Results:docker imagesoutside the build container.--secret id=mysecret,src=...is readable at/run/secrets/mysecretduringRUN, and/run/secretsis not present in the final image.--labeland aLABELfed by--build-arg(theorg.testcontainers.resource-reaper-sessionpattern from the docs) are set on the resulting image.Questions before I write any code
ImageFromDockerfileBuilder, or a separate builder type? Should the CLI image be configurable and pinned the wayComposeBuilder(string image)is?testcontainers/Docker.DotNet(POST /build?version=2&session=<id>plus the gRPC session for filesync, auth and secrets), with this as an interim step? Since the client is a Testcontainers-owned fork that route is not blocked upstream, it is just a lot more work.Benefit
# syntax=frontends,RUN --mount, and platform build args.Alternatives
dockerCLI. Simplest option, but it breaks the API-only design and depends on whatever CLI and buildx version the host happens to have. Not proposed.Docker.DotNet. Best fit with the current architecture, but it needs BuildKit protobufs and a gRPC session over a hijacked connection in .NET, and it holds up both open issues until that is done.WithResourceMappingandWithStartupCallback. Both work, but they move build orchestration out of the test and neither helps with secrets.moby/buildkitcontainer instead ofdocker:28-cli, so a real buildkitd rather than the daemon's builder. More control, but a heavier moving part and further from the Compose approach.Would you like to help contributing this enhancement?
Yes