Repository: PrefectHQ/prefect
Pull request #17866: feat: added option to persist auto-generated Dockerfile
Author: login: AliLordLoss
Labels: name: enhancement
description: An improvement of an existing feature
Created: 2025-04-19T18:50:53Z | Merged: 2025-05-19T15:23:31Z

# 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.



Diff:
diff --git a/src/integrations/prefect-docker/prefect_docker/deployments/steps.py b/src/integrations/prefect-docker/prefect_docker/deployments/steps.py
index 4273b571825e..d7e77a5df1f4 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 = "Dockerfile.generated",
     **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():


Reviews:
user: login: desertaxle
body: Thanks for the PR, @AliLordLoss — this is shaping up nicely!

I have two small requests to help make this even better:

1. In `build_docker_image`, could you add a log message when the generated Dockerfile is saved? It would be helpful to let users know they should update the `dockerfile` field in their configuration, since otherwise a new file will be generated on each run.
2. Could you add a couple of tests to cover the new functionality?

Let me know if you have any questions or need help with either!
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-21T13:43:12Z
user: login: desertaxle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T13:16:18Z
user: login: desertaxle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T13:42:52Z
user: login: desertaxle
body: Hey @AliLordLoss, I think you might have accidentally reverted your code changes instead of the changes to `tests/cli/test_deploy.py` 😅 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T14:06:18Z
user: login: desertaxle
body: Thanks for sticking with the @AliLordLoss! I have one more small nit, but once that's addressed, this will be good to merge!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T19:45:23Z
user: login: desertaxle
body: LGTM! Thanks @AliLordLoss!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T15:23:25Z

Review comments:
diff_hunk: @@ -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(
+                "Persisting auto-generated Dockerfile to disk. You can use `dockerfile_output_path` to specify the output path."
path: src/integrations/prefect-docker/prefect_docker/deployments/steps.py
user: login: desertaxle
body: This is closer to the log message I had in mind:
```suggestion
                f"Persisting auto-generated Dockerfile to {dockerfile_output_path or 'Dockerfile.generated'}. Please update your 'dockerfile' value to use this Dockerfile for subsequent runs."
```
created_at: 2025-04-29T13:16:03Z
author_association: MEMBER
path: tests/cli/test_deploy.py
user: login: desertaxle
body: I think that you can revert the changes to this file. This file is more for covering general `prefect deploy` functionality. The tests you added to `prefect-docker` are enough to cover the new functionality!
created_at: 2025-05-02T13:42:52Z
author_association: MEMBER
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
user: login: desertaxle
body: nit: this will simplify the code a little bit.
```suggestion
    dockerfile_output_path: str = "Dockerfile.generated",
```
created_at: 2025-05-18T19:42:55Z
author_association: MEMBER

PR comments:
user: login: AliLordLoss
created_at: 2025-04-23T08:42:49Z
body: > Thanks for the PR, @AliLordLoss — this is shaping up nicely!
> 
> I have two small requests to help make this even better:
> 
> 1. In `build_docker_image`, could you add a log message when the generated Dockerfile is saved? It would be helpful to let users know they should update the `dockerfile` field in their configuration, since otherwise a new file will be generated on each run.
> 2. Could you add a couple of tests to cover the new functionality?
> 
> Let me know if you have any questions or need help with either!

Hi there @desertaxle, I'll certainly try to address your requests.
Thanks for the guidance, I'll make sure to let you know if I had any sort of problem.
author_association: CONTRIBUTOR
user: login: AliLordLoss
created_at: 2025-04-23T09:58:12Z
body: @desertaxle I added some tests but they're failing, it seems like it's using the old version of the `build_docker_image` rather than the one with my modifications.
Should I modify a different test file? E.g. src/integrations/prefect-docker/tests/deployments/test_steps.py??? I tried doing it but couldn't even run the file, it complained about not being able to import `prefect_docker`.
author_association: CONTRIBUTOR
user: login: desertaxle
created_at: 2025-04-29T13:13:58Z
body: Yep, you should add tests to `src/integrations/prefect-docker/tests/deployments/test_steps.py`. You can install an editable version of `prefect-docker` by going into `src/integrations/prefect-docker` and running `uv sync`. After that, you can run the `prefect-docker` tests by running `uv run pytest tests`.
author_association: MEMBER
user: login: AliLordLoss
created_at: 2025-05-16T10:11:44Z
body: > Hey @AliLordLoss, I think you might have accidentally reverted your code changes instead of the changes to `tests/cli/test_deploy.py` 😅

Yeah, sorry 😅
Fixed it
author_association: CONTRIBUTOR
user: login: AliLordLoss
created_at: 2025-05-19T15:19:44Z
body: > Thanks for sticking with the @AliLordLoss! I have one more small nit, but once that's addressed, this will be good to merge!

You're welcome! That's done as well.
author_association: CONTRIBUTOR