Repository: PrefectHQ/prefect
Pull request: feat: added option to persist auto-generated Dockerfile
# Fix for #12938 

Adds `persist_dockerfile` and `dockerfile_output_path` parameters to the `build_docker_image`
step. This allows users to keep the auto-generated Dockerfile for inspection or reuse.

By default, the Dockerfile is still deleted after the build unless `persist_dockerfile` is set to True.

Closes https://github.com/PrefectHQ/prefect/issues/12938

### Checklist
<!-- These boxes may be checked after opening the pull request. -->

- [x] This pull request references any related issue by including "closes `<link to issue>`"
  - If no issue exists and your change is not a small fix, please [create an issue](https://github.com/PrefectHQ/prefect/issues/new/choose) first.
- [x] If this pull request adds new functionality, it includes unit tests that cover the changes
- [ ] If this pull request removes docs files, it includes redirect settings in `mint.json`.
- [ ] If this pull request adds functions or classes, it includes helpful docstrings.


Problem statement:
Ability to save the Dockerfile created by `build_docker_image` deployment step
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar request and didn't find it.
- [X] I searched the Prefect documentation for this feature.

### Prefect Version

2.x

### Describe the current behavior

No Dockerfile is persisted.

### Describe the proposed behavior

A Dockerfile is persisted.

### Example Use

The user could then inspect the Dockerfile to make deployments with Docker more transparent.

The user could also then modify the Dockerfile and specify the modified Dockerfile in prefect.yaml.

### Additional context

I talked to @desertaxle about this a bit already. 

In my own use and at PACC it was observed that this functionality would be helpful for debugging.

Hints from issue discussion:
I think if we removed deletion of the Dockerfile at the end of `prefect_docker.steps.build_docker_image`, the Dockerfile will persist, but the build step will still have `dockerfile: auto` which would no longer be accurate the next time that the build step was run, as we'd want to use the Dockerfile they have. So we'd want to think about that.
Hi, I would like to work on this issue.

Commit message: Merge branch 'main' into fix-issue-12938

Patch to review:
diff --git a/src/integrations/prefect-docker/prefect_docker/deployments/steps.py b/src/integrations/prefect-docker/prefect_docker/deployments/steps.py
index 4273b571825e..f5ac58004d12 100644
--- a/src/integrations/prefect-docker/prefect_docker/deployments/steps.py
+++ b/src/integrations/prefect-docker/prefect_docker/deployments/steps.py
@@ -127,6 +127,8 @@ def build_docker_image(
     tag: str | None = None,
     additional_tags: list[str] | None = None,
     ignore_cache: bool = False,
+    persist_dockerfile: bool = False,
+    dockerfile_output_path: str | None = None,
     **build_kwargs: Any,
 ) -> BuildDockerImageResult:
     """
@@ -142,6 +144,10 @@ def build_docker_image(
             passed, a temporary Dockerfile will be created to build the image.
         tag: The tag to apply to the built image.
         additional_tags: Additional tags on the image, in addition to `tag`, to apply to the built image.
+        persist_dockerfile: If True and dockerfile="auto", the generated Dockerfile will be saved
+            instead of deleted after the build.
+        dockerfile_output_path: Optional path where the auto-generated Dockerfile should be saved
+            (e.g., "Dockerfile.generated"). Only used if `persist_dockerfile` is True.
         **build_kwargs: Additional keyword arguments to pass to Docker when building
             the image. Available options can be found in the [`docker-py`](https://docker-py.readthedocs.io/en/stable/images.html#docker.models.images.ImageCollection.build)
             documentation.
@@ -187,6 +193,17 @@ def build_docker_image(
                 dockerfile: Dockerfile
                 platform: amd64
         ```
+        Save the auto-generated Dockerfile to disk:
+        ```yaml
+        build:
+          - prefect_docker.deployments.steps.build_docker_image:
+              requires: prefect-docker
+              image_name: repo-name/image-name
+              tag: dev
+              dockerfile: auto
+              persist_dockerfile: true
+              dockerfile_output_path: Dockerfile.generated
+        ```
     """  # noqa
 
     namespace, repository = split_repository_path(image_name)
@@ -221,6 +238,18 @@ def build_docker_image(
         with Path(temp_dockerfile).open("w") as f:
             f.writelines(line + "\n" for line in lines)
 
+        if persist_dockerfile:
+            logger.info(
+                f"Persisting auto-generated Dockerfile to {dockerfile_output_path or 'Dockerfile.generated'}. Please update your 'dockerfile' value to use this Dockerfile for subsequent runs."
+            )
+            output_path = Path(dockerfile_output_path or "Dockerfile.generated")
+            with output_path.open("w") as out_file:
+                out_file.writelines(line + "\n" for line in lines)
+        else:
+            logger.info(
+                "Deleting auto-generated Dockerfile after build. Consider using `persist_dockerfile` to save it."
+            )
+
         dockerfile = str(temp_dockerfile)
 
     build_kwargs["path"] = build_kwargs.get("path", os.getcwd())
diff --git a/src/integrations/prefect-docker/tests/deployments/test_steps.py b/src/integrations/prefect-docker/tests/deployments/test_steps.py
index 03a5832db960..f2f4588f9b31 100644
--- a/src/integrations/prefect-docker/tests/deployments/test_steps.py
+++ b/src/integrations/prefect-docker/tests/deployments/test_steps.py
@@ -149,6 +149,23 @@ def mock_datetime(monkeypatch: pytest.MonkeyPatch):
             },
             "registry/repo:mytag",
         ),
+        (
+            {
+                "image_name": "registry/repo",
+                "dockerfile": "auto",
+                "persist_dockerfile": True,
+            },
+            f"registry/repo:{FAKE_DEFAULT_TAG}",
+        ),
+        (
+            {
+                "image_name": "registry/repo",
+                "dockerfile": "auto",
+                "persist_dockerfile": True,
+                "dockerfile_output_path": "Dockerfile.test",
+            },
+            f"registry/repo:{FAKE_DEFAULT_TAG}",
+        ),
     ],
 )
 @pytest.mark.usefixtures("mock_datetime")
@@ -160,6 +177,8 @@ def test_build_docker_image(
     auto_build = False
     image_name = kwargs.get("image_name")
     dockerfile = kwargs.get("dockerfile", "Dockerfile")
+    persist_dockerfile = kwargs.get("persist_dockerfile", False)
+    dockerfile_output_path = kwargs.get("dockerfile_output_path", None)
     tag = kwargs.get("tag", FAKE_DEFAULT_TAG)
     additional_tags = kwargs.get("additional_tags", None)
     path = kwargs.get("path", os.getcwd())
@@ -202,7 +221,10 @@ def test_build_docker_image(
     mock_docker_client.images.get.assert_called_once_with(FAKE_CONTAINER_ID)
 
     if auto_build:
-        assert not Path("Dockerfile").exists()
+        if persist_dockerfile:
+            assert Path(dockerfile_output_path or "Dockerfile.generated").exists()
+        else:
+            assert not Path("Dockerfile").exists()
 
 
 def test_build_docker_image_raises_with_auto_and_existing_dockerfile():


Code review comments:
diff_hunk: @@ -127,6 +127,8 @@ def build_docker_image(
     tag: str | None = None,
     additional_tags: list[str] | None = None,
     ignore_cache: bool = False,
+    persist_dockerfile: bool = False,
+    dockerfile_output_path: str | None = None,
path: src/integrations/prefect-docker/prefect_docker/deployments/steps.py
text: @user1:
nit: this will simplify the code a little bit.
```suggestion
    dockerfile_output_path: str = "Dockerfile.generated",
```