Repository: PrefectHQ/prefect
Issue category: Feature Request

Improve error message when Docker is not running and Prefect tries to build a Docker image
### 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

Trying to rund `flow.deploy` to build an image results in the following error if Docker is not running:

```bash
Traceback (most recent call last):
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/connectionpool.py", line 793, in urlopen
    response = self._make_request(
               ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/connectionpool.py", line 496, in _make_request
    conn.request(
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/connection.py", line 400, in request
    self.endheaders()
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/http/client.py", line 1289, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/http/client.py", line 1048, in _send_output
    self.send(msg)
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/http/client.py", line 986, in send
    self.connect()
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/transport/unixconn.py", line 27, in connect
    sock.connect(self.unix_socket)
FileNotFoundError: [Errno 2] No such file or directory

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/requests/adapters.py", line 486, in send
    resp = conn.urlopen(
           ^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/connectionpool.py", line 847, in urlopen
    retries = retries.increment(
              ^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/util/retry.py", line 470, in increment
    raise reraise(type(error), error, _stacktrace)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/util/util.py", line 38, in reraise
    raise value.with_traceback(tb)
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/connectionpool.py", line 793, in urlopen
    response = self._make_request(
               ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/connectionpool.py", line 496, in _make_request
    conn.request(
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/urllib3/connection.py", line 400, in request
    self.endheaders()
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/http/client.py", line 1289, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/http/client.py", line 1048, in _send_output
    self.send(msg)
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/http/client.py", line 986, in send
    self.connect()
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/transport/unixconn.py", line 27, in connect
    sock.connect(self.unix_socket)
urllib3.exceptions.ProtocolError: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/api/client.py", line 214, in _retrieve_server_version
    return self.version(api_version=False)["ApiVersion"]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/api/daemon.py", line 181, in version
    return self._result(self._get(url), json=True)
                        ^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/utils/decorators.py", line 46, in inner
    return f(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/api/client.py", line 237, in _get
    return self.get(url, **self._set_request_timeout(kwargs))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/requests/sessions.py", line 602, in get
    return self.request("GET", url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/requests/sessions.py", line 589, in request
    resp = self.send(prep, **send_kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/requests/sessions.py", line 703, in send
    r = adapter.send(request, **kwargs)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/requests/adapters.py", line 501, in send
    raise ConnectionError(err, request=request)
requests.exceptions.ConnectionError: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/jeffhale/Desktop/prefect/demos/deploy_demo/get_weather.py", line 17, in <module>
    fetch_weather.deploy(
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/utilities/asyncutils.py", line 255, in coroutine_wrapper
    return call()
           ^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 398, in __call__
    return self.result()
           ^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 284, in result
    return self.future.result(timeout=timeout)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 168, in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
    raise self._exception
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/_internal/concurrency/calls.py", line 355, in _run_async
    result = await coro
             ^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/flows.py", line 1001, in deploy
    deployment_ids = await deploy(
                     ^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/deployments/runner.py", line 831, in deploy
    image.build()
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/deployments/runner.py", line 698, in build
    build_image(**build_kwargs)
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/contextlib.py", line 81, in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/utilities/dockerutils.py", line 159, in build_image
    with docker_client() as client:
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/contextlib.py", line 137, in __enter__
    return next(self.gen)
           ^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/prefect/utilities/dockerutils.py", line 108, in docker_client
    client = docker.DockerClient.from_env()
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/client.py", line 96, in from_env
    return cls(
           ^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/client.py", line 45, in __init__
    self.api = APIClient(*args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/api/client.py", line 197, in __init__
    self._version = self._retrieve_server_version()
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Caskroom/miniforge/base/lib/python3.11/site-packages/docker/api/client.py", line 221, in _retrieve_server_version
    raise DockerException(
docker.errors.DockerException: Error while fetching server API version: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))
```

### Describe the proposed behavior

Catch the exception and return a more human-readable error that suggests the user check whether Docker is running.

### Example Use

_No response_

### Additional context

We observed several users encounter this error and get confused about its meaning at a PACC. I have also stumbled over it in the past. 

Might want to do something similar for the exception discussed in [issue #9337](https://github.com/PrefectHQ/prefect/issues/9337).


Functions to edit:
src/prefect/utilities/dockerutils.py:docker_client

Patch:
diff --git a/src/prefect/utilities/dockerutils.py b/src/prefect/utilities/dockerutils.py
index 3c05cb7fa2f9..adbe302066de 100644
--- a/src/prefect/utilities/dockerutils.py
+++ b/src/prefect/utilities/dockerutils.py
@@ -104,13 +104,18 @@ def silence_docker_warnings() -> Generator[None, None, None]:
 @contextmanager
 def docker_client() -> Generator["DockerClient", None, None]:
     """Get the environmentally-configured Docker client"""
-    with silence_docker_warnings():
-        client = docker.DockerClient.from_env()
-
+    client = None
     try:
-        yield client
+        with silence_docker_warnings():
+            client = docker.DockerClient.from_env()
+
+            yield client
+    except docker.errors.DockerException as exc:
+        raise RuntimeError(
+            "This error is often thrown because Docker is not running. Please ensure Docker is running."
+        ) from exc
     finally:
-        client.close()
+        client is not None and client.close()
 
 
 class BuildError(Exception):

Test patch: diff --git a/tests/utilities/test_importtools.py b/tests/utilities/test_importtools.py
index bb9f10f77957..7284fd1fafce 100644
--- a/tests/utilities/test_importtools.py
+++ b/tests/utilities/test_importtools.py
@@ -3,6 +3,7 @@
 import sys
 from pathlib import Path
 from types import ModuleType
+from unittest.mock import MagicMock
 from uuid import uuid4
 
 import pytest
@@ -71,6 +72,19 @@ def test_lazy_import():
     assert callable(docker.from_env)
 
 
+@pytest.mark.service("docker")
+def test_cant_find_docker_error(monkeypatch):
+    docker = lazy_import("docker")
+    docker.errors = lazy_import("docker.errors")
+    monkeypatch.setattr(
+        "docker.DockerClient.from_env",
+        MagicMock(side_effect=docker.errors.DockerException),
+    )
+    with pytest.raises(RuntimeError, match="Docker is not running"):
+        with docker_client() as _:
+            return None
+
+
 @pytest.mark.service("docker")
 def test_lazy_import_does_not_break_type_comparisons():
     docker = lazy_import("docker")