Repository: PrefectHQ/prefect

Issue:
set default_url for Docker storage off DOCKER_HOST env var if it exists
# Use Case
Users (or services they use; see context below) may set their docker daemon's host to something besides the default, and if so they are likely using the environment variable `DOCKER_HOST` to point any docker clients to the correct location. 

# Solution
Set Docker storage's `default_url` more by trying to check a user's `DOCKER_HOST` env var before defaulting to the hardcoded defaults listed already. See example below as well.

## Archived from the [Prefect Public Slack Community](https://join.slack.com/t/prefect-public/shared_invite/enQtNzE5OTU3OTQwNzc1LTQ5M2FkZmQzZjI0ODg1ZTBmOTc0ZjVjYWFjMWExZDAyYzBmYjVmMTE1NTQ1Y2IxZTllOTc4MmI3NzYxMDlhYWU)

**jschmid**: Question for folks doing CI/CD automation to build &amp; register Prefect Flows. Building Docker storage for one of our flows from our CI/CD system (<http://Screwdriver.io|Screwdriver.io>) fails with the exception below. Docker appears to be installed and working properly because `docker run hello-world` succeeds. (There is special "docker-in-docker" config we use with Screwdriver and this works well for many of our other projects.) I can also build Docker storage successfully on my laptop from inside the same image that our CI/CD system is using. I'm sure it's some simple config that I'm missing -- anybody run into this?

```
15:25:50 Traceback (most recent call last):
15:25:50   File "/usr/local/lib/python3.7/runpy.py", line 193, in _run_module_as_main
15:25:50     "__main__", mod_spec)
15:25:50   File "/usr/local/lib/python3.7/runpy.py", line 85, in _run_code
15:25:50     exec(code, run_globals)
15:25:50   File "/usr/local/lib/python3.7/site-packages/srm_datasci-0.1.0-py3.7.egg/srm/datasci/flows/util/test_flow.py", line 35, in &lt;module&gt;
15:25:50     flow.register("Utilities")
15:25:50   File "/usr/local/lib/python3.7/site-packages/prefect/core/flow.py", line 1412, in register
15:25:50     no_url=no_url,
15:25:50   File "/usr/local/lib/python3.7/site-packages/prefect/client/client.py", line 616, in register
15:25:50     serialized_flow = flow.serialize(build=build)  # type: Any
15:25:50   File "/usr/local/lib/python3.7/site-packages/prefect/core/flow.py", line 1221, in serialize
15:25:50     storage = self.storage.build()  # type: Optional[Storage]
15:25:50   File "/usr/local/lib/python3.7/site-packages/prefect/environments/storage/docker.py", line 263, in build
15:25:50     self._build_image(push=push)
15:25:50   File "/usr/local/lib/python3.7/site-packages/prefect/environments/storage/docker.py", line 293, in _build_image
15:25:50     self.pull_image()
15:25:50   File "/usr/local/lib/python3.7/site-packages/prefect/environments/storage/docker.py", line 483, in pull_image
15:25:50     client = self._get_client()
15:25:50   File "/usr/local/lib/python3.7/site-packages/prefect/environments/storage/docker.py", line 471, in _get_client
15:25:50     return docker.APIClient(base_url=self.base_url, version="auto")
15:25:50   File "/usr/local/lib/python3.7/site-packages/docker/api/client.py", line 190, in __init__
15:25:50     self._version = self._retrieve_server_version()
15:25:50   File "/usr/local/lib/python3.7/site-packages/docker/api/client.py", line 215, in _retrieve_server_version
15:25:50     'Error while fetching server API version: {0}'.format(e)
15:25:50 docker.errors.DockerException: Error while fetching server API version: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))
```


**jschmid**: I suspect it's related to this line in the Prefect Docker storage code: <https://github.com/PrefectHQ/prefect/blob/master/src/prefect/environments/storage/docker.py#L96>)

**jschmid**: Was able to work through this. Updating here in case anyone runs into something similar. Screwdriver configures their "docker-in-docker" setup with DOCKER_HOST set to `<tcp://localhost:2375>`  So for us the change was:

```
from prefect.environments.storage import Docker

storage = Docker(
    registry_url="&lt;redacted&gt;",
    image_name="&lt;redacted&gt;",
    image_tag=IMAGE_TAG,
    python_dependencies=["&lt;redacted&gt;"],
    base_url=environ.get("DOCKER_HOST"),
)
```

It might be good to bubble this change down into Prefect, e.g:

```
    default_url = environ.get("DOCKER_HOST", "<unix://var/run/docker.sock>")
```

Here: <https://github.com/PrefectHQ/prefect/blob/master/src/prefect/environments/storage/docker.py#L96>

**laura**: ah, you beat me (though my only contribution was to note about base_url being an option for you to configure once you knew what your CI’s docker api endpoint really was)

**laura**: I like your idea, reading docker docs briefly it looks like DOCKER_HOST is an env var natively supported by the docker client if not  dockerd itself

**jschmid**: Thanks <@US4FKJEGH>, that all makes sense!

**laura**: I’m going to try out the issue bot :slightly_smiling_face:

**laura**: hmm its not that

**laura**: <@ULVA73B9P> archive “set default_url for Docker storage off DOCKER_HOST env var if it exists”




Discussion:

Resolving patch:
diff --git a/src/prefect/environments/storage/docker.py b/src/prefect/environments/storage/docker.py
--- a/src/prefect/environments/storage/docker.py
+++ b/src/prefect/environments/storage/docker.py
@@ -57,7 +57,7 @@ class Docker(Storage):
         - files (dict, optional): a dictionary of files to copy into the image
             when building
         - base_url: (str, optional): a URL of a Docker daemon to use when for
-            Docker related functionality
+            Docker related functionality.  Defaults to DOCKER_HOST env var if not set
         - prefect_version (str, optional): an optional branch, tag, or commit
             specifying the version of prefect you want installed into the container;
             defaults to the version you are currently using or `"master"` if your
@@ -92,12 +92,10 @@ def __init__(
         secrets: List[str] = None,
     ) -> None:
         self.registry_url = registry_url
-
         if sys.platform == "win32":
             default_url = "npipe:////./pipe/docker_engine"
         else:
             default_url = "unix://var/run/docker.sock"
-
         self.image_name = image_name
         self.image_tag = image_tag
         self.python_dependencies = python_dependencies or []
@@ -111,7 +109,7 @@ def __init__(
         self.files = files or {}
         self.flows = dict()  # type: Dict[str, str]
         self._flows = dict()  # type: Dict[str, "prefect.core.flow.Flow"]
-        self.base_url = base_url or default_url
+        self.base_url = base_url or os.environ.get("DOCKER_HOST", default_url)
         self.local_image = local_image
         self.extra_commands = []  # type: List[str]
         self.ignore_healthchecks = ignore_healthchecks


Test patch:
diff --git a/tests/environments/storage/test_docker_storage.py b/tests/environments/storage/test_docker_storage.py
--- a/tests/environments/storage/test_docker_storage.py
+++ b/tests/environments/storage/test_docker_storage.py
@@ -13,6 +13,15 @@
 from prefect.environments.storage import Docker
 
 
+@pytest.fixture
+def no_docker_host_var(monkeypatch):
+    """
+    This fixture is for tests that assert an unset DOCKER_HOST variable
+    to avoid muddying test results from running Docker in Docker (e.g. in CI)
+    """
+    monkeypatch.delenv("DOCKER_HOST", raising=False)
+
+
 def test_create_docker_storage():
     storage = Docker(secrets=["cloud_creds"])
     assert storage
@@ -49,7 +58,7 @@ def test_add_flow_to_docker():
         ("darwn", "unix://var/run/docker.sock"),
     ],
 )
-def test_empty_docker_storage(monkeypatch, platform, url):
+def test_empty_docker_storage(monkeypatch, platform, url, no_docker_host_var):
     monkeypatch.setattr("prefect.environments.storage.docker.sys.platform", platform)
     monkeypatch.setattr(sys, "version_info", MagicMock(major=3, minor=6))
     monkeypatch.setattr(prefect, "__version__", "0.9.2+c2394823")
@@ -78,7 +87,9 @@ def test_empty_docker_storage(monkeypatch, platform, url):
         ("darwn", "unix://var/run/docker.sock"),
     ],
 )
-def test_empty_docker_storage_on_tagged_commit(monkeypatch, platform, url):
+def test_empty_docker_storage_on_tagged_commit(
+    monkeypatch, platform, url, no_docker_host_var
+):
     monkeypatch.setattr("prefect.environments.storage.docker.sys.platform", platform)
     monkeypatch.setattr(sys, "version_info", MagicMock(major=3, minor=6))
     monkeypatch.setattr(prefect, "__version__", "0.9.2")
@@ -122,7 +133,7 @@ def test_docker_init_responds_to_prefect_version(monkeypatch, version):
     assert storage.prefect_version == version[1]
 
 
-def test_initialized_docker_storage():
+def test_initialized_docker_storage(no_docker_host_var):
     storage = Docker(
         registry_url="test1",
         base_image="test3",
@@ -149,6 +160,16 @@ def test_initialized_docker_storage():
     assert storage.local_image
 
 
+def test_env_var_precedence_docker_storage(monkeypatch, no_docker_host_var):
+    monkeypatch.setenv("DOCKER_HOST", "bar")
+    storage = Docker()
+    assert storage.base_url
+    assert storage.base_url == "bar"
+    storage = Docker(base_url="foo")
+    assert storage.base_url == "foo"
+    storage = Docker(base_url="foo")
+
+
 def test_docker_storage_allows_for_user_provided_config_locations():
     storage = Docker(env_vars={"PREFECT__USER_CONFIG_PATH": "1"})
 
@@ -477,7 +498,7 @@ def test_create_dockerfile_with_weird_flow_name():
             )
 
 
-def test_create_dockerfile_from_everything():
+def test_create_dockerfile_from_everything(no_docker_host_var):
 
     with tempfile.TemporaryDirectory() as tempdir_outside: