Repository: PrefectHQ/prefect

Issue:
Prompt for Docker information when a Docker-based infrastructure type is selected during interactive `prefect deploy` run
### Summary

When a user has selected a Docker-based infrastructure type (e.g. `docker`, `ecs`, `cloud-run`) during an interactive `prefect deploy` session, prompt the user for information about building and pushing a Docker image for their deployed flow.

### Details

The CLI can determine if a work pool supports Docker images by inspecting its `base_job_template`. If the work pool has a `image` variable, it can be assumed to support use of custom Docker images.

If the user selects a Docker-based infrastructure type, the CLI should follow the following flow:
1. Ask the user if they would like to build a custom Docker image.
2. If a Dockerfile is present in the current directory, ask them if they would like to use that Dockerfile
3. Ask for an image name
4. Ask for an image tag
5. Ask the user if they would like to push this image to a remote registry
6. Ask which registry they would like to push to
7. Ask if it is a private registry
8. Ask for credentials to use for pushing

The answers to these questions should be use to configure the necessary steps to build and push a Docker image. If a Dockerfile is absent, the auto-build feature available in the `build_docker_image` should be used. A `DockerRegistryCredentials` block should be created if credentials are provided.


Discussion:

Resolving patch:
diff --git a/src/prefect/cli/_prompts.py b/src/prefect/cli/_prompts.py
--- a/src/prefect/cli/_prompts.py
+++ b/src/prefect/cli/_prompts.py
@@ -2,8 +2,14 @@
 Utilities for prompting the user for input
 """
 from datetime import timedelta
+from getpass import GetPassWarning
+import os
+import shutil
+import sys
 from prefect.deployments.base import _search_for_flow_functions
 from prefect.flows import load_flow_from_entrypoint
+from prefect.infrastructure.container import DockerRegistry
+from prefect.utilities.processutils import run_process
 from rich.prompt import PromptBase, InvalidResponse
 from rich.text import Text
 from rich.progress import Progress, SpinnerColumn, TextColumn
@@ -32,6 +38,8 @@
 
 from prefect.client.schemas.actions import WorkPoolCreate
 
+from prefect.utilities.slugify import slugify
+
 
 def prompt(message, **kwargs):
     """Utility to prompt the user for input with consistent styling"""
@@ -345,6 +353,163 @@ async def prompt_select_work_pool(
         return selected_work_pool_row["name"]
 
 
+async def prompt_build_custom_docker_image(
+    console: Console,
+    deployment_config: dict,
+):
+    if not confirm(
+        "Would you like to build a custom Docker image for this deployment?",
+        console=console,
+        default=False,
+    ):
+        return
+
+    build_step = {
+        "requires": "prefect-docker>=0.3.1",
+        "id": "build-image",
+    }
+
+    if os.path.exists("Dockerfile"):
+        if confirm(
+            "Would you like to use the Dockerfile in the current directory?",
+            console=console,
+            default=True,
+        ):
+            build_step["dockerfile"] = "Dockerfile"
+        else:
+            if confirm(
+                "A Dockerfile exists but you chose not to use it. Would you like to"
+                " rename it?"
+            ):
+                new_dockerfile_name = prompt(
+                    "New Dockerfile name", default="Dockerfile.backup"
+                )
+                shutil.move("Dockerfile", new_dockerfile_name)
+                build_step["dockerfile"] = "auto"
+            else:
+                # this will otherwise raise when build steps are run as the auto-build feature
+                # executed in the build_docker_image step will create a temporary Dockerfile
+                raise ValueError(
+                    "A Dockerfile already exists. Please remove or rename the existing"
+                    " one."
+                )
+    else:
+        build_step["dockerfile"] = "auto"
+
+    repo_name = prompt("Repository name (e.g. your Docker Hub username)").rstrip("/")
+    image_name = prompt("Image name", default=deployment_config["name"])
+    build_step["image_name"] = f"{repo_name}/{image_name}"
+    build_step["tag"] = prompt("Image tag", default="latest")
+
+    console.print(
+        "Image"
+        f" [bold][yellow]{build_step['image_name']}:{build_step['tag']}[/yellow][/bold]"
+        " will be built."
+    )
+
+    return {"prefect_docker.deployments.steps.build_docker_image": build_step}
+
+
+async def prompt_push_custom_docker_image(
+    console: Console,
+    deployment_config: dict,
+    build_docker_image_step: dict,
+):
+    if not confirm(
+        "Would you like to push this image to a remote registry?",
+        console=console,
+        default=False,
+    ):
+        return None, build_docker_image_step
+
+    push_step = {
+        "requires": "prefect-docker>=0.3.1",
+        "image_name": "{{ build-image.image_name }}",
+        "tag": "{{ build-image.tag }}",
+    }
+
+    registry_url = prompt("Registry URL", default="docker.io").rstrip("/")
+
+    repo_and_image_name = build_docker_image_step[
+        "prefect_docker.deployments.steps.build_docker_image"
+    ]["image_name"]
+    full_image_name = f"{registry_url}/{repo_and_image_name}"
+    build_docker_image_step["prefect_docker.deployments.steps.build_docker_image"][
+        "image_name"
+    ] = full_image_name
+
+    if confirm("Is this a private registry?", console=console):
+        docker_credentials = {}
+        docker_credentials["registry_url"] = registry_url
+
+        if confirm(
+            "Would you like use prefect-docker to manage Docker registry credentials?",
+            console=console,
+            default=False,
+        ):
+            try:
+                import prefect_docker
+            except ImportError:
+                console.print("Installing prefect-docker...")
+                await run_process(
+                    [sys.executable, "-m", "pip", "install", "prefect-docker"],
+                    stream_output=True,
+                )
+                import prefect_docker
+
+            credentials_block = prefect_docker.DockerRegistryCredentials
+            push_step["credentials"] = (
+                "{{ prefect_docker.docker-registry-credentials.docker_registry_creds_name }}"
+            )
+        else:
+            credentials_block = DockerRegistry
+            push_step["credentials"] = (
+                "{{ prefect.docker-registry.docker_registry_creds_name }}"
+            )
+        docker_registry_creds_name = f"deployment-{slugify(deployment_config['name'])}-{slugify(deployment_config['work_pool']['name'])}-registry-creds"
+        create_new_block = False
+        try:
+            await credentials_block.load(docker_registry_creds_name)
+            if not confirm(
+                (
+                    "Would you like to use the existing Docker registry credentials"
+                    f" block {docker_registry_creds_name}?"
+                ),
+                console=console,
+                default=True,
+            ):
+                create_new_block = True
+        except ValueError:
+            create_new_block = True
+
+        if create_new_block:
+            docker_credentials["username"] = prompt(
+                "Docker registry username", console=console
+            )
+            try:
+                docker_credentials["password"] = prompt(
+                    "Docker registry password",
+                    console=console,
+                    password=True,
+                )
+            except GetPassWarning:
+                docker_credentials["password"] = prompt(
+                    "Docker registry password",
+                    console=console,
+                )
+
+            new_creds_block = credentials_block(
+                username=docker_credentials["username"],
+                password=docker_credentials["password"],
+                registry_url=docker_credentials["registry_url"],
+            )
+            await new_creds_block.save(name=docker_registry_creds_name, overwrite=True)
+
+    return {
+        "prefect_docker.deployments.steps.push_docker_image": push_step
+    }, build_docker_image_step
+
+
 @inject_client
 async def prompt_create_work_pool(
     console: Console,
diff --git a/src/prefect/cli/deploy.py b/src/prefect/cli/deploy.py
--- a/src/prefect/cli/deploy.py
+++ b/src/prefect/cli/deploy.py
@@ -24,6 +24,8 @@
     prompt_schedule,
     prompt_select_work_pool,
     prompt_entrypoint,
+    prompt_build_custom_docker_image,
+    prompt_push_custom_docker_image,
 )
 from prefect.cli.root import app, is_interactive
 from prefect.client.schemas.schedules import (
@@ -434,6 +436,54 @@ async def _run_single_deploy(
             console=app.console, client=client
         )
 
+    docker_build_steps = [
+        "prefect_docker.deployments.steps.build_docker_image",
+        "prefect_docker.projects.steps.build_docker_image",
+    ]
+
+    docker_build_step_exists = any(
+        any(step in action for step in docker_build_steps)
+        for action in deploy_config.get("build", actions.get("build")) or []
+    )
+
+    update_work_pool_image = False
+
+    if is_interactive() and not docker_build_step_exists:
+        work_pool = await client.read_work_pool(deploy_config["work_pool"]["name"])
+        docker_based_infrastructure = "image" in work_pool.base_job_template.get(
+            "variables", {}
+        ).get("properties", {})
+        if docker_based_infrastructure:
+            build_docker_image_step = await prompt_build_custom_docker_image(
+                app.console, deploy_config
+            )
+            if build_docker_image_step is not None:
+                work_pool_job_variables_image_not_found = not get_from_dict(
+                    deploy_config, "work_pool.job_variables.image"
+                )
+                if work_pool_job_variables_image_not_found:
+                    update_work_pool_image = True
+
+                push_docker_image_step, updated_build_docker_image_step = (
+                    await prompt_push_custom_docker_image(
+                        app.console, deploy_config, build_docker_image_step
+                    )
+                )
+
+                if actions.get("build"):
+                    actions["build"].append(updated_build_docker_image_step)
+                else:
+                    actions["build"] = [updated_build_docker_image_step]
+
+                if push_docker_image_step is not None:
+                    if actions.get("push"):
+                        actions["push"].append(push_docker_image_step)
+                    else:
+                        actions["push"] = [push_docker_image_step]
+
+            build_steps = deploy_config.get("build", actions.get("build")) or []
+            push_steps = deploy_config.get("push", actions.get("push")) or []
+
     triggers: List[DeploymentTrigger] = []
     trigger_specs = deploy_config.get("triggers")
     if trigger_specs:
@@ -466,9 +516,19 @@ async def _run_single_deploy(
 
     step_outputs.update(variable_overrides)
 
+    if update_work_pool_image:
+        if "build-image" not in step_outputs:
+            app.console.print(
+                "Warning: no build-image step found in the deployment build steps."
+                " The work pool image will not be updated."
+            )
+        deploy_config["work_pool"]["job_variables"]["image"] = "{{ build-image.image }}"
+
     if not deploy_config.get("description"):
         deploy_config["description"] = flow.description
 
+    # save deploy_config before templating
+    deploy_config_before_templating = deepcopy(deploy_config)
     ## apply templating from build and push steps to the final deployment spec
     _parameter_schema = deploy_config.pop("parameter_openapi_schema")
     _schedule = deploy_config.pop("schedule")
@@ -530,25 +590,27 @@ async def _run_single_deploy(
     ):
         matching_deployment_exists = (
             _check_for_matching_deployment_name_and_entrypoint_in_prefect_file(
-                deploy_config=deploy_config
+                deploy_config=deploy_config_before_templating
             )
         )
         if matching_deployment_exists and not confirm(
             (
                 "Found existing deployment configuration with name:"
-                f" [yellow]{deploy_config.get('name')}[/yellow] and entrypoint:"
-                f" [yellow]{deploy_config.get('entrypoint')}[/yellow] in the"
-                " [yellow]prefect.yaml[/yellow] file. Would you like to overwrite that"
-                " entry?"
+                f" [yellow]{deploy_config_before_templating.get('name')}[/yellow] and"
+                " entrypoint:"
+                f" [yellow]{deploy_config_before_templating.get('entrypoint')}[/yellow]"
+                " in the [yellow]prefect.yaml[/yellow] file. Would you like to"
+                " overwrite that entry?"
             ),
         ):
             app.console.print(
                 "[red]Cancelled saving deployment configuration"
-                f" '{deploy_config.get('name')}' to the prefect.yaml file.[/red]"
+                f" '{deploy_config_before_templating.get('name')}' to the prefect.yaml"
+                " file.[/red]"
             )
         else:
             _save_deployment_to_prefect_file(
-                deploy_config,
+                deploy_config_before_templating,
                 build_steps=build_steps or None,
                 push_steps=push_steps or None,
                 pull_steps=pull_steps or None,


Test patch:
diff --git a/tests/cli/test_deploy.py b/tests/cli/test_deploy.py
--- a/tests/cli/test_deploy.py
+++ b/tests/cli/test_deploy.py
@@ -12,6 +12,8 @@
 from prefect.cli.deploy import (
     _check_for_matching_deployment_name_and_entrypoint_in_prefect_file,
 )
+from prefect.infrastructure.container import DockerRegistry
+from prefect.utilities.slugify import slugify
 import pytest
 import readchar
 from typer import Exit
@@ -125,6 +127,54 @@ async def default_agent_pool(prefect_client):
     )
 
 
+@pytest.fixture
+async def docker_work_pool(prefect_client: PrefectClient):
+    return await prefect_client.create_work_pool(
+        work_pool=WorkPoolCreate(
+            name="test-docker-work-pool",
+            type="docker",
+            base_job_template={
+                "job_configuration": {"image": "{{ image}}"},
+                "variables": {
+                    "type": "object",
+                    "properties": {
+                        "image": {
+                            "title": "Image",
+                            "type": "string",
+                        },
+                    },
+                },
+            },
+        )
+    )
+
+
+@pytest.fixture
+async def mock_prompt(monkeypatch):
+    # Mock prompts() where password=True to prevent hanging
+    def new_prompt(message, password=False, **kwargs):
+        if password:
+            return "456"
+        else:
+            return original_prompt(message, password=password, **kwargs)
+
+    original_prompt = prefect.cli._prompts.prompt
+    monkeypatch.setattr("prefect.cli._prompts.prompt", new_prompt)
+
+
+@pytest.fixture
+def mock_build_docker_image(monkeypatch):
+    mock_build = mock.MagicMock()
+    mock_build.return_value = {"build-image": {"image": "{{ build-image.image }}"}}
+
+    monkeypatch.setattr(
+        "prefect.deployments.steps.core.import_object",
+        lambda x: mock_build,
+    )
+
+    return mock_build
+
+
 class TestProjectDeploySingleDeploymentYAML:
     """
     Tests for projects where deployment.yaml contains only one deployment
@@ -1581,9 +1631,7 @@ async def test_deploy_with_prefect_agent_work_pool_interactive(
         assert deployment.entrypoint == "./flows/hello.py:my_flow"
 
     @pytest.mark.usefixtures("interactive_console", "project_dir")
-    async def test_deploy_with_no_available_work_pool_interactive(
-        self, prefect_client, default_agent_pool
-    ):
+    async def test_deploy_with_no_available_work_pool_interactive(self, prefect_client):
         await run_sync_in_worker_thread(
             invoke_and_assert,
             command="deploy ./flows/hello.py:my_flow -n test-name --interval 3600",
@@ -3969,3 +4017,965 @@ async def test_deploy_command_warns_triggers_not_created_not_cloud(
                 "Deployment triggers are only supported on Prefect Cloud"
             ],
         )
+
+
+@pytest.mark.usefixtures("project_dir", "interactive_console", "work_pool")
+class TestDeployDockerBuildSteps:
+    async def test_docker_build_step_exists_does_not_prompt_build_custom_docker_image(
+        self,
+        docker_work_pool,
+        mock_build_docker_image,
+    ):
+        prefect_file = Path("prefect.yaml")
+        with prefect_file.open(mode="r") as f:
+            prefect_config = yaml.safe_load(f)
+
+        with open("Dockerfile", "w") as f:
+            f.write("FROM python:3.8-slim\n")
+
+        prefect_config["build"] = [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "requires": "prefect-docker",
+                    "image_name": "local/repo",
+                    "tag": "dev",
+                    "id": "build-image",
+                    "dockerfile": "Dockerfile",
+                }
+            }
+        ]
+
+        # save it back
+        with prefect_file.open(mode="w") as f:
+            yaml.safe_dump(prefect_config, f)
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600 -p"
+                f" {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept save configuration
+                "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_does_not_contain=[
+                "Would you like to build a custom Docker image"
+            ],
+        )
+        assert result.exit_code == 0
+        assert "An important name/test" in result.output
+
+        with prefect_file.open(mode="r") as f:
+            prefect_config = yaml.safe_load(f)
+
+    async def test_other_build_step_exists_prompts_build_custom_docker_image(
+        self,
+        docker_work_pool,
+    ):
+        prefect_file = Path("prefect.yaml")
+        with prefect_file.open(mode="r") as f:
+            prefect_config = yaml.safe_load(f)
+
+        prefect_config["build"] = [
+            {
+                "prefect.deployments.steps.run_shell_script": {
+                    "id": "get-commit-hash",
+                    "script": "git rev-parse --short HEAD",
+                    "stream_output": False,
+                }
+            }
+        ]
+
+        # save it back
+        with prefect_file.open(mode="w") as f:
+            yaml.safe_dump(prefect_config, f)
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Reject build custom docker image
+                "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Would you like to save configuration for this deployment",
+            ],
+        )
+        assert result.exit_code == 0
+        assert "An important name/test" in result.output
+
+        prefect_file = Path("prefect.yaml")
+
+        with open(prefect_file, "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert not config["deployments"][1].get("build")
+
+    async def test_no_build_step_exists_prompts_build_custom_docker_image(
+        self, docker_work_pool
+    ):
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Reject build custom docker image
+                "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Would you like to save configuration for this deployment",
+            ],
+        )
+        assert result.exit_code == 0
+        assert "An important name/test" in result.output
+
+        prefect_file = Path("prefect.yaml")
+
+        with open(prefect_file, "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert not config["deployments"][1].get("build")
+
+    async def test_prompt_build_custom_docker_image_accepted_use_existing_dockerfile_accepted(
+        self, docker_work_pool, mock_build_docker_image
+    ):
+        with open("Dockerfile", "w") as f:
+            f.write("FROM python:3.8-slim\n")
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Accept use existing dockerfile
+                + "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                +
+                # Default image_name
+                readchar.key.ENTER
+                +
+                # Default tag
+                readchar.key.ENTER
+                +
+                # Reject push to registry
+                "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Would you like to use the Dockerfile in the current directory?",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Is this a private registry?"],
+        )
+
+        assert result.exit_code == 0
+        assert "An important name/test" in result.output
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "Dockerfile",
+                    "image_name": "prefecthq/prefect/test-name",
+                    "tag": "latest",
+                }
+            }
+        ]
+
+    async def test_prompt_build_custom_docker_image_accepted_use_existing_dockerfile_rejected_rename_accepted(
+        self, docker_work_pool, monkeypatch, mock_build_docker_image
+    ):
+        with open("Dockerfile", "w") as f:
+            f.write("FROM python:3.8-slim\n")
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Reject use existing dockerfile
+                + "n"
+                + readchar.key.ENTER
+                # Accept rename dockerfile
+                + "y"
+                + readchar.key.ENTER
+                +
+                # Enter new dockerfile name
+                "Dockerfile.backup"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                +
+                # Default image_name
+                readchar.key.ENTER
+                +
+                # Default tag
+                readchar.key.ENTER
+                +
+                # Reject push to registry
+                "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Would you like to use the Dockerfile in the current directory?",
+                "A Dockerfile exists but you chose not to use it.",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Is this a private registry?"],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": "prefecthq/prefect/test-name",
+                    "tag": "latest",
+                }
+            }
+        ]
+
+    async def test_prompt_build_custom_docker_image_accepted_use_existing_dockerfile_rejected_rename_rejected(
+        self, docker_work_pool
+    ):
+        with open("Dockerfile", "w") as f:
+            f.write("FROM python:3.8-slim\n")
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Reject use existing dockerfile
+                + "n"
+                + readchar.key.ENTER
+                # Accept rename dockerfile
+                + "n"
+                + readchar.key.ENTER
+            ),
+            expected_code=1,
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Would you like to use the Dockerfile in the current directory?",
+                "A Dockerfile exists but you chose not to use it.",
+                (
+                    "A Dockerfile already exists. Please remove or rename the existing"
+                    " one."
+                ),
+            ],
+            expected_output_does_not_contain=["Is this a private registry?"],
+        )
+
+        assert result.exit_code == 1
+
+    async def test_prompt_build_custom_docker_image_accepted_no_existing_dockerfile_uses_auto_build(
+        self, docker_work_pool, monkeypatch, mock_build_docker_image
+    ):
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Reject push to registry
+                + "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Is this a private registry?"],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": "prefecthq/prefect/test-name",
+                    "tag": "latest",
+                }
+            }
+        ]
+
+    async def test_no_existing_work_pool_image_gets_updated_after_adding_build_docker_image_step(
+        self, docker_work_pool, monkeypatch, mock_build_docker_image
+    ):
+        prefect_file = Path("prefect.yaml")
+        if prefect_file.exists():
+            prefect_file.unlink()
+        assert not prefect_file.exists()
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Reject push to registry
+                + "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Is this a private registry?"],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 1
+        assert config["deployments"][0]["name"] == "test-name"
+        assert config["deployments"][0]["work_pool"]["name"] == docker_work_pool.name
+        assert (
+            config["deployments"][0]["work_pool"]["job_variables"]["image"]
+            == "{{ build-image.image }}"
+        )
+        assert config["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": "prefecthq/prefect/test-name",
+                    "tag": "latest",
+                }
+            }
+        ]
+
+    async def test_work_pool_image_already_exists_not_updated_after_adding_build_docker_image_step(
+        self, docker_work_pool, monkeypatch, mock_build_docker_image
+    ):
+        prefect_file = Path("prefect.yaml")
+        with open("prefect.yaml", "w") as f:
+            contents = {
+                "work_pool": {
+                    "name": docker_work_pool.name,
+                    "job_variables": {"image": "original-image"},
+                }
+            }
+            yaml.dump(contents, f)
+        assert prefect_file.exists()
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Reject push to registry
+                + "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Is this a private registry?"],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 1
+        assert config["deployments"][0]["name"] == "test-name"
+        assert config["deployments"][0]["work_pool"]["name"] == docker_work_pool.name
+        assert (
+            config["deployments"][0]["work_pool"]["job_variables"]["image"]
+            == "{{ build-image.image }}"
+        )
+        assert config["work_pool"] == {
+            "name": docker_work_pool.name,
+            "job_variables": {"image": "original-image"},
+        }
+
+
+@pytest.mark.usefixtures("project_dir", "interactive_console", "work_pool")
+class TestDeployDockerPushSteps:
+    async def test_prompt_push_custom_docker_image_rejected(
+        self, docker_work_pool, monkeypatch, mock_build_docker_image
+    ):
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Reject push to registry
+                + "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Is this a private registry?"],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": "prefecthq/prefect/test-name",
+                    "tag": "latest",
+                }
+            }
+        ]
+        assert not config["deployments"][1].get("push")
+
+    async def test_prompt_push_custom_docker_image_accepted_public_registry(
+        self, docker_work_pool, monkeypatch, mock_build_docker_image
+    ):
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Accept push to registry
+                + "y"
+                + readchar.key.ENTER
+                # Registry URL
+                + "https://hub.docker.com"
+                + readchar.key.ENTER
+                # Reject private registry
+                + "n"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Is this a private registry?",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=[
+                "Would you like use prefect-docker to manage Docker registry"
+                " credentials?"
+            ],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": "https://hub.docker.com/prefecthq/prefect/test-name",
+                    "tag": "latest",
+                }
+            }
+        ]
+
+        assert config["deployments"][1]["push"] == [
+            {
+                "prefect_docker.deployments.steps.push_docker_image": {
+                    "requires": "prefect-docker>=0.3.1",
+                    "image_name": "{{ build-image.image_name }}",
+                    "tag": "{{ build-image.tag }}",
+                }
+            }
+        ]
+
+    async def test_prompt_push_docker_image_accepted_private_registry_use_existing_core_creds(
+        self, docker_work_pool, monkeypatch, mock_build_docker_image
+    ):
+        docker_registry_creds_name = f"deployment-{slugify('test-name')}-{slugify(docker_work_pool.name)}-registry-creds"
+
+        # create a DockerRegistry block so we can use existing credentials
+        await DockerRegistry(
+            username="abc",
+            password="123",
+            registry_url="https://private.docker.com",
+        ).save(name=docker_registry_creds_name, overwrite=True)
+
+        assert await DockerRegistry.load(docker_registry_creds_name)
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Accept push to registry
+                + "y"
+                + readchar.key.ENTER
+                # Registry URL
+                + "https://private.docker.com"
+                + readchar.key.ENTER
+                # Accept private registry
+                + "y"
+                + readchar.key.ENTER
+                # Reject use prefect-docker
+                + "n"
+                + readchar.key.ENTER
+                # Accept use existing creds
+                + "y"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Is this a private registry?",
+                (
+                    "Would you like use prefect-docker to manage Docker registry"
+                    " credentials?"
+                ),
+                "Would you like to use the existing Docker registry credentials",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Installing prefect-docker..."],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": (
+                        "https://private.docker.com/prefecthq/prefect/test-name"
+                    ),
+                    "tag": "latest",
+                }
+            }
+        ]
+
+        assert config["deployments"][1]["push"] == [
+            {
+                "prefect_docker.deployments.steps.push_docker_image": {
+                    "requires": "prefect-docker>=0.3.1",
+                    "image_name": "{{ build-image.image_name }}",
+                    "tag": "{{ build-image.tag }}",
+                    "credentials": (
+                        "{{ prefect.docker-registry.docker_registry_creds_name }}"
+                    ),
+                }
+            }
+        ]
+
+        with pytest.raises(ImportError):
+            import prefect_docker  # noqa
+
+    async def test_prompt_push_docker_image_accepted_private_registry_use_new_core_creds(
+        self, docker_work_pool, monkeypatch, mock_prompt, mock_build_docker_image
+    ):
+        # ensure the DockerRegistry block does not exist
+        docker_registry_creds_name = f"deployment-{slugify('test-name')}-{slugify(docker_work_pool.name)}-registry-creds"
+        with pytest.raises(ValueError):
+            await DockerRegistry.load(docker_registry_creds_name)
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Accept push to registry
+                + "y"
+                + readchar.key.ENTER
+                # Registry URL
+                + "https://private.docker.com"
+                + readchar.key.ENTER
+                # Accept private registry
+                + "y"
+                + readchar.key.ENTER
+                # Reject use prefect-docker
+                + "n"
+                + readchar.key.ENTER
+                # Enter username
+                + "abc"
+                + readchar.key.ENTER
+                # Enter password
+                + "456"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Is this a private registry?",
+                (
+                    "Would you like use prefect-docker to manage Docker registry"
+                    " credentials?"
+                ),
+                "Docker registry username",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=[
+                "Would you like to use the existing Docker registry credentials",
+                "Installing prefect-docker...",
+            ],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": (
+                        "https://private.docker.com/prefecthq/prefect/test-name"
+                    ),
+                    "tag": "latest",
+                }
+            }
+        ]
+
+        assert config["deployments"][1]["push"] == [
+            {
+                "prefect_docker.deployments.steps.push_docker_image": {
+                    "requires": "prefect-docker>=0.3.1",
+                    "image_name": "{{ build-image.image_name }}",
+                    "tag": "{{ build-image.tag }}",
+                    "credentials": (
+                        "{{ prefect.docker-registry.docker_registry_creds_name }}"
+                    ),
+                }
+            }
+        ]
+
+        new_block = await DockerRegistry.load(docker_registry_creds_name)
+
+        assert new_block.username == "abc"
+        assert new_block.password.get_secret_value() == "456"
+        assert new_block.registry_url == "https://private.docker.com"
+
+        with pytest.raises(ImportError):
+            import prefect_docker  # noqa
+
+    async def test_prompt_push_docker_image_accepted_private_registry_reject_use_existing_core_creds(
+        self, docker_work_pool, monkeypatch, mock_prompt, mock_build_docker_image
+    ):
+        docker_registry_creds_name = f"deployment-{slugify('test-name')}-{slugify(docker_work_pool.name)}-registry-creds"
+
+        # create a DockerRegistry block so we can reject existing credentials
+        docker_registry_block = DockerRegistry(
+            username="abc",
+            password="123",
+            registry_url="https://private.docker.com",
+        )
+        await docker_registry_block.save(
+            name=docker_registry_creds_name, overwrite=True
+        )
+
+        assert await DockerRegistry.load(docker_registry_creds_name)
+
+        result = await run_sync_in_worker_thread(
+            invoke_and_assert,
+            command=(
+                "deploy ./flows/hello.py:my_flow -n test-name --interval 3600"
+                f" -p {docker_work_pool.name}"
+            ),
+            user_input=(
+                # Accept build custom docker image
+                "y"
+                + readchar.key.ENTER
+                # Enter repo name
+                + "prefecthq/prefect"
+                + readchar.key.ENTER
+                # Default image_name
+                + readchar.key.ENTER
+                # Default tag
+                + readchar.key.ENTER
+                # Accept push to registry
+                + "y"
+                + readchar.key.ENTER
+                # Registry URL
+                + "https://private2.docker.com"
+                + readchar.key.ENTER
+                # Accept private registry
+                + "y"
+                + readchar.key.ENTER
+                # Reject use prefect-docker
+                + "n"
+                + readchar.key.ENTER
+                # Reject use existing creds
+                + "n"
+                + readchar.key.ENTER
+                # Enter username
+                + "def"
+                + readchar.key.ENTER
+                # Enter password
+                + "456"
+                + readchar.key.ENTER
+                # Accept save configuration
+                + "y"
+                + readchar.key.ENTER
+            ),
+            expected_output_contains=[
+                "Would you like to build a custom Docker image",
+                "Image prefecthq/prefect/test-name:latest will be built",
+                "Would you like to push this image to a remote registry?",
+                "Is this a private registry?",
+                (
+                    "Would you like use prefect-docker to manage Docker registry"
+                    " credentials?"
+                ),
+                "Would you like to use the existing Docker registry credentials",
+                "Docker registry username",
+                "Would you like to save configuration for this deployment",
+            ],
+            expected_output_does_not_contain=["Installing prefect-docker..."],
+        )
+
+        assert result.exit_code == 0
+
+        with open("prefect.yaml", "r") as f:
+            config = yaml.safe_load(f)
+
+        assert len(config["deployments"]) == 2
+        assert config["deployments"][1]["name"] == "test-name"
+        assert config["deployments"][1]["build"] == [
+            {
+                "prefect_docker.deployments.steps.build_docker_image": {
+                    "id": "build-image",
+                    "requires": "prefect-docker>=0.3.1",
+                    "dockerfile": "auto",
+                    "image_name": (
+                        "https://private2.docker.com/prefecthq/prefect/test-name"
+                    ),
+                    "tag": "latest",
+                }
+            }
+        ]
+
+        assert config["deployments"][1]["push"] == [
+            {
+                "prefect_docker.deployments.steps.push_docker_image": {
+                    "requires": "prefect-docker>=0.3.1",
+                    "image_name": "{{ build-image.image_name }}",
+                    "tag": "{{ build-image.tag }}",
+                    "credentials": (
+                        "{{ prefect.docker-registry.docker_registry_creds_name }}"
+                    ),
+                }
+            }
+        ]
+
+        new_block = await DockerRegistry.load(docker_registry_creds_name)
+
+        assert new_block.username == "def"
+        assert new_block.password != "123"
+        assert new_block.password.get_secret_value() == "456"
+        assert new_block.registry_url == "https://private2.docker.com"
+
+        with pytest.raises(ImportError):
+            import prefect_docker  # noqa