plans/completed/2026-03-25-buildkit-support.md
Add opt-in BuildKit support to Prefect's Docker image building by introducing python-on-whales as an alternative build backend. Users who need BuildKit features (secrets, SSH forwarding, multi-platform builds, --mount syntax) can opt in via a build_backend="buildx" parameter while the default docker-py path remains unchanged.
DockerWorker — only build and push are affected.--mount syntax fail entirely through docker-py.prefect dev build-image already shells out to the Docker CLI via subprocess, establishing precedent for CLI-based builds in this codebase.Two build paths exist today, both using docker-py:
| Path | Entry point | Builds via |
|---|---|---|
| SDK | flow.deploy() → DockerImage.build() | dockerutils.build_image() → client.api.build() |
| YAML steps | prefect deploy → build_docker_image() | client.api.build() directly |
This plan adds a parallel buildx path to both. A new module src/prefect/docker/buildx.py encapsulates all python-on-whales interactions. The existing code dispatches to it based on a build_backend parameter.
DockerImage(name="myimage", build_backend="buildx", platforms=["linux/amd64"])
│
├── build_backend="docker-py" (default) ──→ dockerutils.build_image() ──→ docker-py
│
└── build_backend="buildx" ──→ buildx.build_image() ──→ python-on-whales ──→ docker CLI
python-on-whales is an optional dependency, not required for default operation:
pyproject.toml under a buildx extra: buildx = ["python-on-whales>=0.81"]prefect-docker's optional deps as wellbuildx.py — if the user sets build_backend="buildx" without installing the extra, raise a clear error with install instructionssrc/prefect/docker/buildx.pyThe python-on-whales backend module. Contains:
buildx_build_image(context, dockerfile, tag, pull, platform, stream_progress_to, push, **kwargs) -> str — wraps python_on_whales.docker.buildx.build(). Returns the image ID (via --iidfile). Accepts buildx-native kwargs like secrets, ssh, cache_from, cache_to, platforms. When push=True, passes --push to buildx so the build-and-push happens in a single step (required for multi-platform builds where the result is a manifest list, not a local image).buildx_push_image(name, tag, stream_progress_to) -> None — wraps python_on_whales.docker.image.push(). Used when pushing a previously-built single-platform image.ImportError with install instructions if python-on-whales is missing.Design notes:
IMAGE_LABELS ({"io.prefect.version": ...}) must be applied to buildx builds too, via the labels parameter.docker.buildx.build(push=True) is the only viable path — buildx cannot load multi-platform results into the local daemon. When platforms has multiple entries and push=False, raise a clear error explaining this constraint.dockerutils.build_image should be reused. Factor the retry wrapper into a shared helper or have buildx.build_image implement its own.src/prefect/docker/docker_image.pyAdd build_backend: str = "docker-py" parameter to DockerImage.__init__. Store as self.build_backend.
In build():
self.build_backend == "docker-py": existing path (call build_image() from dockerutils)self.build_backend == "buildx": call buildx_build_image() from the new moduleIn push():
self.build_backend == "docker-py": existing pathself.build_backend == "buildx": call buildx_push_image()Build-and-push optimization: When build_backend="buildx", DockerImage should support a push=True kwarg in build() that passes --push to buildx, performing build and push as a single operation. This is required for multi-platform builds and more efficient for single-platform builds. When push=True is passed to build(), the subsequent push() call should be a no-op.
The build_kwargs passthrough changes meaning based on backend:
"docker-py" → kwargs go to docker-py's client.api.build()"buildx" → kwargs go to python_on_whales.docker.buildx.build()Document this clearly in the DockerImage docstring and ensure invalid kwargs produce helpful errors.
src/integrations/prefect-docker/prefect_docker/deployments/steps.pyAdd build_backend: str = "docker-py" parameter to build_docker_image().
When build_backend="buildx":
buildx_build_image() instead of client.api.build()push_docker_image() also gets a build_backend parameter — when "buildx", use buildx_push_image() instead of docker-py's pushpyproject.tomlAdd optional extra:
[project.optional-dependencies]
buildx = ["python-on-whales>=0.81"]
src/integrations/prefect-docker/pyproject.tomlAdd optional extra:
[project.optional-dependencies]
buildx = ["python-on-whales>=0.81"]
tests/docker/test_buildx.py (new)python_on_whales to test buildx_build_image() and buildx_push_image() without a Docker daemonIMAGE_LABELS are passed throughtests/docker/test_docker_image.py (modify)DockerImage(build_backend="buildx") dispatching to buildx_build_imagebuild_kwargs are forwarded correctly for both backendsbuild_backend value raises ValueErrorsrc/integrations/prefect-docker/tests/deployments/test_steps.py (modify)build_docker_image(build_backend="buildx")buildx_build_image with correct kwargstests/docker/test_image_builds.py (modify)@pytest.mark.service("docker") tests that build a real image via the buildx backendbuild_backend="buildx". Default remains "docker-py". Document in deployment docs."buildx" the default when python-on-whales is installed (auto-detect).build_backend — keep it parameter-only for now. A PREFECT_DOCKER_BUILD_BACKEND setting can be added in a follow-up if there's demand.push=True in build() to run docker buildx build --push. This is required for multi-platform builds and more efficient for single-platform.python-on-whales>=0.81 (current latest release).