Repository: PrefectHQ/prefect

Issue:
Allow Deployment to point to a flow entrypoint baked into a Docker image (Docker storage)
### 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 proposed behavior

- Some users try to skip remote file system block as they build docker image anyway
- While this is technically already possible to point to a path on the container, this doesn't work with the current build which expects to upload the flow code 

Changes needed:
- modify path to point to /opt/prefect/root_flows_dir
- use `--skip-upload`

### Describe the current behavior

...

### Example Use

_No response_

### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/src/prefect/cli/deployment.py b/src/prefect/cli/deployment.py
--- a/src/prefect/cli/deployment.py
+++ b/src/prefect/cli/deployment.py
@@ -427,6 +427,12 @@ async def build(
         "-o",
         help="An optional filename to write the deployment file to.",
     ),
+    _apply: bool = typer.Option(
+        False,
+        "--apply",
+        "-a",
+        help="An optional flag to automatically register the resulting deployment with the API.",
+    ),
 ):
     """
     Generate a deployment YAML from /path/to/file.py:flow_function
@@ -517,6 +523,18 @@ async def build(
     else:
         storage = None
 
+    ## docker default settings
+    # proxy for whether infra is docker-based
+    is_docker_based = hasattr(infrastructure, "image")
+
+    if not storage:
+        if is_docker_based:
+            # only update if a path is not already set
+            if not path:
+                path = "/opt/prefect/flows"
+        else:
+            path = str(Path(".").absolute())
+
     # set up deployment object
     deployment = Deployment(name=name, flow_name=flow.name)
     await deployment.load()  # load server-side settings, if any
@@ -557,6 +575,29 @@ async def build(
 
     deployment_loc = output_file or f"{obj_name}-deployment.yaml"
     await deployment.to_yaml(deployment_loc)
-    exit_with_success(
-        f"Deployment YAML created at '{Path(deployment_loc).absolute()!s}'."
+    app.console.print(
+        f"Deployment YAML created at '{Path(deployment_loc).absolute()!s}'.",
+        style="green",
     )
+
+    if _apply:
+        deployment_id = await deployment.apply()
+        app.console.print(
+            f"Deployment '{deployment.flow_name}/{deployment.name}' successfully created with id '{deployment_id}'.",
+            style="green",
+        )
+        if deployment.work_queue_name is not None:
+            app.console.print(
+                "\nTo execute flow runs from this deployment, start an agent "
+                f"that pulls work from the the {deployment.work_queue_name!r} work queue:"
+            )
+            app.console.print(
+                f"$ prefect agent start -q {deployment.work_queue_name!r}", style="blue"
+            )
+        else:
+            app.console.print(
+                "\nThis deployment does not specify a work queue name, which means agents "
+                "will not be able to pick up its runs. To add a work queue, "
+                "edit the deployment spec and re-run this command, or visit the deployment in the UI.",
+                style="red",
+            )
diff --git a/src/prefect/deployments.py b/src/prefect/deployments.py
--- a/src/prefect/deployments.py
+++ b/src/prefect/deployments.py
@@ -511,6 +511,7 @@ async def build_from_flow(
         name: str,
         output: str = None,
         skip_upload: bool = False,
+        apply: bool = False,
         **kwargs,
     ) -> "Deployment":
         """
@@ -525,6 +526,7 @@ async def build_from_flow(
             output (optional): if provided, the full deployment specification will be written as a YAML
                 file in the location specified by `output`
             skip_upload: if True, deployment files are not automatically uploaded to remote storage
+            apply: if True, the deployment is automatically registered with the API
             **kwargs: other keyword arguments to pass to the constructor for the `Deployment` class
         """
         if not name:
@@ -555,8 +557,16 @@ async def build_from_flow(
             deployment.version = flow.version
         if not deployment.description:
             deployment.description = flow.description
-        if not deployment.storage:
+
+        # proxy for whether infra is docker-based
+        is_docker_based = hasattr(deployment.infrastructure, "image")
+
+        if not deployment.storage and not is_docker_based:
             deployment.path = str(Path(".").absolute())
+        elif not deployment.storage and is_docker_based:
+            # only update if a path is not already set
+            if not deployment.path:
+                deployment.path = "/opt/prefect/flows"
 
         if not skip_upload:
             await deployment.upload_to_storage()
@@ -564,4 +574,7 @@ async def build_from_flow(
         if output:
             await deployment.to_yaml(output)
 
+        if apply:
+            await deployment.apply()
+
         return deployment


Test patch:
diff --git a/tests/cli/test_deployment_cli.py b/tests/cli/test_deployment_cli.py
--- a/tests/cli/test_deployment_cli.py
+++ b/tests/cli/test_deployment_cli.py
@@ -132,6 +132,31 @@ def test_version_flag_takes_precedence(self, patch_import, tmp_path):
         deployment = Deployment.load_from_yaml(tmp_path / "test.yaml")
         assert deployment.version == "CLI-version"
 
+    def test_auto_apply_flag(self, patch_import, tmp_path):
+        d = Deployment(
+            name="TEST",
+            flow_name="fn",
+        )
+        deployment_id = d.apply()
+
+        invoke_and_assert(
+            [
+                "deployment",
+                "build",
+                "fake-path.py:fn",
+                "-n",
+                "TEST",
+                "-o",
+                str(tmp_path / "test.yaml"),
+                "--apply",
+            ],
+            expected_code=0,
+            expected_output_contains=[
+                f"Deployment '{d.flow_name}/{d.name}' successfully created with id '{deployment_id}'."
+            ],
+            temp_dir=tmp_path,
+        )
+
 
 class TestOutputMessages:
     def test_message_with_work_queue_name(self, patch_import, tmp_path):
diff --git a/tests/orion/api/test_run_history.py b/tests/orion/api/test_run_history.py
--- a/tests/orion/api/test_run_history.py
+++ b/tests/orion/api/test_run_history.py
@@ -506,6 +506,7 @@ async def test_last_bin_contains_end_date(client, route):
     assert parsed[1].interval_end == dt.add(days=2)
 
 
+@pytest.mark.flaky(max_runs=3)
 async def test_flow_run_lateness(client, session):
 
     await session.execute("delete from flow where true;")
diff --git a/tests/test_deployments.py b/tests/test_deployments.py
--- a/tests/test_deployments.py
+++ b/tests/test_deployments.py
@@ -5,7 +5,7 @@
 from prefect.blocks.core import Block
 from prefect.deployments import Deployment
 from prefect.filesystems import S3, LocalFileSystem
-from prefect.infrastructure import Process
+from prefect.infrastructure import DockerContainer, Process
 
 
 class TestDeploymentBasicInterface:
@@ -210,6 +210,34 @@ async def test_build_from_flow_sets_path(self, flow_function, skip_upload):
         assert d.name == "foo"
         assert d.path is not None
 
+    async def test_build_from_flow_manages_docker_specific_path(self, flow_function):
+        # default, local within-container storage assumption
+        d = await Deployment.build_from_flow(
+            flow=flow_function, name="foo", infrastructure=DockerContainer()
+        )
+        assert d.path == "/opt/prefect/flows"
+
+        # can be overriden
+        d = await Deployment.build_from_flow(
+            flow=flow_function,
+            name="foo",
+            infrastructure=DockerContainer(),
+            path="/my/path/of/choice",
+        )
+        assert d.path == "/my/path/of/choice"
+
+        # remote storage, path is unimportant
+        for path in [None, "subdir"]:
+            d = await Deployment.build_from_flow(
+                flow=flow_function,
+                name="foo",
+                infrastructure=DockerContainer(),
+                storage=S3(bucket_path="unreal"),
+                skip_upload=True,
+                path=path,
+            )
+            assert d.path == path
+
     async def test_build_from_flow_sets_description_and_version_if_not_set(
         self, flow_function
     ):