Repository: beeware/briefcase

Issue:
Silence the recommendations/hints from Docker Desktop
### Describe the bug

Using Docker, as of late, includes a lot of unhelpful recommendations being printed to the console.

For instance:
```
(venv) user@vm-win11:~/tmp/beeware/helloworld$ briefcase build --target fedora

[Docker] Downloading Docker base image for fedora...
Using default tag: latest
latest: Pulling from library/fedora
8237fce9fd6b: Pull complete
Digest: sha256:06df381d697d14940c886fda8e94a4fdc838df74e93f65111ed3ea04f7a7d6e0
Status: Downloaded newer image for fedora:latest
docker.io/library/fedora:latest

What's Next?
  View a summary of image vulnerabilities and recommendations → docker scout quickview fedora
```

We should disable these given they aren't useful for our use-case.

### Steps to reproduce

Run Docker-related commands with Docker Desktop.

### Expected behavior

Use environment variable `DOCKER_CLI_HINTS=false` to disable this output.

### Screenshots

_No response_

### Environment

- Operating System: Windows 11
- Python version: 3.8
- Software versions:
  - Briefcase: `0.3.17.dev283+g23daa64e`

### Logs

_No response_

### Additional context

_No response_

Discussion:
rmartin16: This approach adds a `final_kwargs()` to `Docker` and assumes it will be used anytime `subprocess` is being used to invoke `docker`. This excludes the `docker` invocations during `verify()`...but they could be added if `final_kwargs()` is made a Class method. I avoided doing this, though, because I'd also like to create a `Protocol` for all these interfaces that are modeled on `Subprocess`.

At any rate, another approach I considered was updating `dockerize_args()` to support invoking `docker` for commands other than `docker run`. While that works, it kinda started to overload `dockerize_args()`, IMO.

Thoughts or other ideas welcome.

Patch:
diff --git a/changes/1635.feature.rst b/changes/1635.feature.rst
new file mode 100644
index 00000000..f8f974f7
--- /dev/null
+++ b/changes/1635.feature.rst
@@ -0,0 +1,1 @@
+The hints and recommendations that Docker prints in the console are now silenced.
diff --git a/src/briefcase/integrations/docker.py b/src/briefcase/integrations/docker.py
index 9c807867..d8f1f4f4 100644
--- a/src/briefcase/integrations/docker.py
+++ b/src/briefcase/integrations/docker.py
@@ -167,7 +167,10 @@ See https://docs.docker.com/go/buildx/ to install the buildx plugin.
             # Try to get the version of docker that is installed.
             # expected output format: Docker version 25.0.2, build 29cf629\n
             docker_version = (
-                tools.subprocess.check_output(["docker", "--version"])
+                tools.subprocess.check_output(
+                    ["docker", "--version"],
+                    env=cls.subprocess_env(),
+                )
                 .split("Docker version ")[1]
                 .split(",")[0]
             )
@@ -200,7 +203,10 @@ See https://docs.docker.com/go/buildx/ to install the buildx plugin.
             # Invoke a docker command to check if the daemon is running,
             # and the user has sufficient permissions.
             # We don't care about the output, just that it succeeds.
-            tools.subprocess.check_output(["docker", "info"])
+            tools.subprocess.check_output(
+                ["docker", "info"],
+                env=cls.subprocess_env(),
+            )
         except subprocess.CalledProcessError as e:
             failure_output = e.output
             if "permission denied while trying to connect" in failure_output:
@@ -219,7 +225,10 @@ See https://docs.docker.com/go/buildx/ to install the buildx plugin.
     def _buildx_installed(cls, tools: ToolCache):
         """Verify the buildx plugin is installed."""
         try:
-            tools.subprocess.check_output(["docker", "buildx", "version"])
+            tools.subprocess.check_output(
+                ["docker", "buildx", "version"],
+                env=cls.subprocess_env(),
+            )
         except subprocess.CalledProcessError:
             raise BriefcaseCommandError(cls.BUILDX_PLUGIN_MISSING)
 
@@ -347,13 +356,14 @@ Delete this file and run Briefcase again.
         :param image_tag: Image name/tag to pull if not locally cached
         """
         image_id = self.tools.subprocess.check_output(
-            ["docker", "images", "-q", image_tag]
+            ["docker", "images", "-q", image_tag],
+            env=self.subprocess_env(),
         ).strip()
 
         if not image_id:
             self.tools.logger.info(
                 f"Downloading Docker base image for {image_tag}...",
-                prefix="Docker",
+                prefix=self.full_name,
             )
             try:
                 # disable streaming so image download progress bar is shown
@@ -361,6 +371,7 @@ Delete this file and run Briefcase again.
                     ["docker", "pull", image_tag],
                     check=True,
                     stream_output=False,
+                    env=self.subprocess_env(),
                 )
             except subprocess.CalledProcessError as e:
                 raise BriefcaseCommandError(
@@ -388,6 +399,17 @@ Delete this file and run Briefcase again.
             **self.dockerize_args(args, image_tag=image_tag, **kwargs)
         )
 
+    @classmethod
+    def subprocess_env(cls, env: dict[str, str] | None = None) -> dict[str, str]:
+        """Adds environment variables to the context that Docker runs in."""
+        final_env = {
+            # Disable the hints/recommendations that Docker prints in the console
+            "DOCKER_CLI_HINTS": "false",
+        }
+        if env is not None:
+            final_env.update(env)
+        return final_env
+
     def dockerize_args(
         self,
         args: SubprocessArgsT,
@@ -460,7 +482,9 @@ Delete this file and run Briefcase again.
         # Finally, add the command (and its arguments) to run in the container
         docker_cmdline.extend(self.dockerize_path(arg, path_map) for arg in args)
 
+        # Augment configuration to drive the subprocess call
         subprocess_kwargs["args"] = docker_cmdline
+        subprocess_kwargs["env"] = self.subprocess_env()
 
         return subprocess_kwargs
 
@@ -504,7 +528,7 @@ Delete this file and run Briefcase again.
         through a Docker-provided mapping of the host's network interface to the spoofed
         display that finally proxies those commands to the actual display.
 
-        Full docs: https://briefcase.readthedocs.io/en/stable/how-to/internal/x11passthrough.html
+        Full docs: https://briefcase.readthedocs.io/en/latest/how-to/internal/x11passthrough.html
 
         :param subprocess_kwargs: Existing keywords args from the caller
         :returns: augmented keyword args for the call to subprocess


Test patch:
diff --git a/tests/integrations/docker/conftest.py b/tests/integrations/docker/conftest.py
index 0c5725f8..0af268d1 100644
--- a/tests/integrations/docker/conftest.py
+++ b/tests/integrations/docker/conftest.py
@@ -69,6 +69,8 @@ def mock_docker(mock_tools, monkeypatch) -> Docker:
 
     mock_tools.docker = Docker(mock_tools)
 
+    mock_tools.os.environ = {"PROCESS_ENV_VAR": "VALUE"}
+
     # Reset the mock so that the user mapping calls don't appear in test results
     mock_tools.subprocess._subprocess.check_output.reset_mock()
 
@@ -111,7 +113,10 @@ def user_mapping_run_calls(tmp_path, monkeypatch) -> list[call]:
         MagicMock(return_value=tmp_path / "build/mock_write_test"),
     )
     return [
-        call(["docker", "images", "-q", "alpine"]),
+        call(
+            ["docker", "images", "-q", "alpine"],
+            env={"DOCKER_CLI_HINTS": "false"},
+        ),
         call(
             args=[
                 "docker",
@@ -123,6 +128,7 @@ def user_mapping_run_calls(tmp_path, monkeypatch) -> list[call]:
                 "touch",
                 "/host_write_test/mock_write_test",
             ],
+            env={"DOCKER_CLI_HINTS": "false"},
         ),
         call(
             args=[
@@ -136,5 +142,6 @@ def user_mapping_run_calls(tmp_path, monkeypatch) -> list[call]:
                 "-f",
                 "/host_write_test/mock_write_test",
             ],
+            env={"DOCKER_CLI_HINTS": "false"},
         ),
     ]
diff --git a/tests/integrations/docker/test_DockerAppContext__Popen.py b/tests/integrations/docker/test_DockerAppContext__Popen.py
index 4f7f0604..7cdc59c8 100644
--- a/tests/integrations/docker/test_DockerAppContext__Popen.py
+++ b/tests/integrations/docker/test_DockerAppContext__Popen.py
@@ -19,7 +19,11 @@ def test_simple_call(mock_tools, my_app, tmp_path, sub_kw, capsys):
     mock_tools[my_app].app_context._dockerize_args.assert_called_once_with(
         ["hello", "world"]
     )
-    mock_tools.subprocess._subprocess.Popen.assert_called_once_with(ANY, **sub_kw)
+    mock_tools.subprocess._subprocess.Popen.assert_called_once_with(
+        ANY,
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
+        **sub_kw,
+    )
     assert capsys.readouterr().out == ""
 
 
@@ -45,6 +49,7 @@ def test_call_with_extra_kwargs(mock_tools, my_app, tmp_path, capsys):
         encoding="ISO-42",
         text=True,
         errors="backslashreplace",
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
     )
     assert capsys.readouterr().out == ""
 
@@ -61,5 +66,9 @@ def test_simple_verbose_call(mock_tools, my_app, tmp_path, sub_kw, capsys):
     mock_tools[my_app].app_context._dockerize_args.assert_called_once_with(
         ["hello", "world"]
     )
-    mock_tools.subprocess._subprocess.Popen.assert_called_once_with(ANY, **sub_kw)
+    mock_tools.subprocess._subprocess.Popen.assert_called_once_with(
+        ANY,
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
+        **sub_kw,
+    )
     assert ">>> Running Command:\n" in capsys.readouterr().out
diff --git a/tests/integrations/docker/test_DockerAppContext__check_output.py b/tests/integrations/docker/test_DockerAppContext__check_output.py
index 13bdde58..6897ee1f 100644
--- a/tests/integrations/docker/test_DockerAppContext__check_output.py
+++ b/tests/integrations/docker/test_DockerAppContext__check_output.py
@@ -20,7 +20,9 @@ def test_simple_call(mock_tools, my_app, tmp_path, sub_check_output_kw, capsys):
         ["hello", "world"]
     )
     mock_tools.subprocess._subprocess.check_output.assert_called_with(
-        ANY, **sub_check_output_kw
+        ANY,
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
+        **sub_check_output_kw,
     )
     assert capsys.readouterr().out == ""
 
@@ -48,6 +50,7 @@ def test_call_with_extra_kwargs(mock_tools, my_app, tmp_path, capsys):
         stderr=subprocess.STDOUT,
         text=True,
         errors="backslashreplace",
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
     )
     assert capsys.readouterr().out == ""
 
@@ -71,6 +74,8 @@ def test_simple_verbose_call(
         ["hello", "world"]
     )
     mock_tools.subprocess._subprocess.check_output.assert_called_once_with(
-        ANY, **sub_check_output_kw
+        ANY,
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
+        **sub_check_output_kw,
     )
     assert ">>> Running Command:\n" in capsys.readouterr().out
diff --git a/tests/integrations/docker/test_DockerAppContext__dockerize_args.py b/tests/integrations/docker/test_DockerAppContext__dockerize_args.py
index 5a33de6d..93b1bf81 100644
--- a/tests/integrations/docker/test_DockerAppContext__dockerize_args.py
+++ b/tests/integrations/docker/test_DockerAppContext__dockerize_args.py
@@ -23,6 +23,7 @@ def test_dockerize_args(mock_tools, my_app, tmp_path):
             "hello",
             "world",
         ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -48,6 +49,7 @@ def test_dockerize_args_sys_executable(mock_tools, my_app, tmp_path):
             "-m",
             "pip",
         ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -80,6 +82,7 @@ def test_dockerize_args_mounts(mock_tools, my_app, tmp_path):
             "hello",
             "world",
         ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -114,6 +117,7 @@ def test_dockerize_args_mounts_path(mock_tools, my_app, tmp_path):
             "world",
             "/container/second/bin",
         ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -144,6 +148,7 @@ def test_dockerize_args_cwd(mock_tools, my_app, tmp_path):
             "hello",
             "world",
         ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -180,6 +185,7 @@ def test_dockerize_args_arg_and_env(mock_tools, my_app, tmp_path):
         ],
         check=True,
         steam_output=True,
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -223,4 +229,5 @@ def test_dockerize_args_path_arg_and_env(mock_tools, my_app, tmp_path):
             "hello",
             os.fsdecode(tmp_path / "location"),
         ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
diff --git a/tests/integrations/docker/test_DockerAppContext__run.py b/tests/integrations/docker/test_DockerAppContext__run.py
index 51fe3b99..062d16ec 100644
--- a/tests/integrations/docker/test_DockerAppContext__run.py
+++ b/tests/integrations/docker/test_DockerAppContext__run.py
@@ -21,7 +21,9 @@ def test_simple_call(mock_tools, my_app, tmp_path, sub_stream_kw, capsys):
     )
     # calls to run() default to using Popen() due to output streaming
     mock_tools.subprocess._subprocess.Popen.assert_called_once_with(
-        ANY, **sub_stream_kw
+        ANY,
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
+        **sub_stream_kw,
     )
     assert capsys.readouterr().out == (
         "\n"
@@ -46,7 +48,11 @@ def test_interactive(mock_tools, my_app, tmp_path, sub_kw, capsys):
         stream_output=False,
     )
     # Interactive means the call to run is direct, rather than going through Popen
-    mock_tools.subprocess._subprocess.run.assert_called_once_with(ANY, **sub_kw)
+    mock_tools.subprocess._subprocess.run.assert_called_once_with(
+        ANY,
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
+        **sub_kw,
+    )
     assert capsys.readouterr().out == (
         "\n"
         "Entering Docker context...\n"
@@ -89,6 +95,7 @@ def test_call_with_extra_kwargs(
         bufsize=1,
         text=True,
         errors="backslashreplace",
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
     )
     assert capsys.readouterr().out == (
         "\n"
@@ -112,7 +119,9 @@ def test_simple_verbose_call(mock_tools, my_app, tmp_path, sub_stream_kw, capsys
         ["hello", "world"],
     )
     mock_tools.subprocess._subprocess.Popen.assert_called_once_with(
-        ANY, **sub_stream_kw
+        ANY,
+        env={"DOCKER_CLI_HINTS": "false", "PROCESS_ENV_VAR": "VALUE"},
+        **sub_stream_kw,
     )
     console_output = capsys.readouterr().out
     assert "Entering Docker context...\n" in console_output
diff --git a/tests/integrations/docker/test_Docker__cache_image.py b/tests/integrations/docker/test_Docker__cache_image.py
index e1dd20a3..9f4b11e4 100644
--- a/tests/integrations/docker/test_Docker__cache_image.py
+++ b/tests/integrations/docker/test_Docker__cache_image.py
@@ -17,12 +17,21 @@ def test_cache_image(mock_tools, sub_kw, sub_check_output_kw):
 
     # Confirms that image is not available
     mock_tools.subprocess._subprocess.check_output.assert_called_with(
-        ["docker", "images", "-q", "ubuntu:jammy"], **sub_check_output_kw
+        ["docker", "images", "-q", "ubuntu:jammy"],
+        env={"PROCESS_ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"},
+        **sub_check_output_kw,
     )
 
     # Image is pulled and cached
     mock_tools.subprocess._subprocess.run.assert_has_calls(
-        [call(["docker", "pull", "ubuntu:jammy"], check=True, **sub_kw)]
+        [
+            call(
+                ["docker", "pull", "ubuntu:jammy"],
+                check=True,
+                env={"PROCESS_ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"},
+                **sub_kw,
+            ),
+        ]
     )
 
 
@@ -37,7 +46,9 @@ def test_cache_image_already_cached(mock_tools, sub_check_output_kw):
 
     # Confirms that image is not available
     mock_tools.subprocess._subprocess.check_output.assert_called_with(
-        ["docker", "images", "-q", "ubuntu:jammy"], **sub_check_output_kw
+        ["docker", "images", "-q", "ubuntu:jammy"],
+        env={"PROCESS_ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"},
+        **sub_check_output_kw,
     )
 
     # Image is not pulled and cached
diff --git a/tests/integrations/docker/test_Docker__check_output.py b/tests/integrations/docker/test_Docker__check_output.py
index 5f552116..49aaf50c 100644
--- a/tests/integrations/docker/test_Docker__check_output.py
+++ b/tests/integrations/docker/test_Docker__check_output.py
@@ -19,10 +19,15 @@ def test_check_output(mock_tools, sub_check_output_kw):
     mock_tools.subprocess._subprocess.check_output.assert_has_calls(
         [
             # Verify image is cached in Docker
-            call(["docker", "images", "-q", "ubuntu:jammy"], **sub_check_output_kw),
+            call(
+                ["docker", "images", "-q", "ubuntu:jammy"],
+                env={"PROCESS_ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"},
+                **sub_check_output_kw,
+            ),
             # Run command in Docker using image
             call(
                 ["docker", "run", "--rm", "ubuntu:jammy", "cmd", "arg1", "arg2"],
+                env={"PROCESS_ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"},
                 **sub_check_output_kw,
             ),
         ]
@@ -47,10 +52,15 @@ def test_check_output_fail(mock_tools, sub_check_output_kw):
     mock_tools.subprocess._subprocess.check_output.assert_has_calls(
         [
             # Verify image is cached in Docker
-            call(["docker", "images", "-q", "ubuntu:jammy"], **sub_check_output_kw),
+            call(
+                ["docker", "images", "-q", "ubuntu:jammy"],
+                env={"PROCESS_ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"},
+                **sub_check_output_kw,
+            ),
             # Command errors in Docker using image
             call(
                 ["docker", "run", "--rm", "ubuntu:jammy", "cmd", "arg1", "arg2"],
+                env={"PROCESS_ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"},
                 **sub_check_output_kw,
             ),
         ]
diff --git a/tests/integrations/docker/test_Docker__dockerize_args.py b/tests/integrations/docker/test_Docker__dockerize_args.py
index 468cb947..711e768a 100644
--- a/tests/integrations/docker/test_Docker__dockerize_args.py
+++ b/tests/integrations/docker/test_Docker__dockerize_args.py
@@ -12,7 +12,10 @@ def test_dockerize_simple_call(mock_tools):
         image_tag="best-image",
     )
 
-    assert args == dict(args=["docker", "run", "--rm", "best-image", "hello", "world"])
+    assert args == dict(
+        args=["docker", "run", "--rm", "best-image", "hello", "world"],
+        env={"DOCKER_CLI_HINTS": "false"},
+    )
 
 
 @pytest.mark.usefixtures("mock_docker")
@@ -25,7 +28,8 @@ def test_dockerize_interactive(mock_tools):
     )
 
     assert args == dict(
-        args=["docker", "run", "--rm", "-it", "best-image", "hello", "world"]
+        args=["docker", "run", "--rm", "-it", "best-image", "hello", "world"],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -53,7 +57,8 @@ def test_dockerize_mounts(mock_tools):
             "best-image",
             "hello",
             "world",
-        ]
+        ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -80,7 +85,8 @@ def test_dockerize_cwd(mock_tools, cwd):
             "best-image",
             "hello",
             "world",
-        ]
+        ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -120,7 +126,8 @@ def test_dockerize_env(mock_tools):
             "best-image",
             "hello",
             "world",
-        ]
+        ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -151,7 +158,8 @@ def test_dockerize_add_hosts(mock_tools):
             "best-image",
             "hello",
             "world",
-        ]
+        ],
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
@@ -208,7 +216,8 @@ DOCKERIZE_TEST_PARAMS = [
                 "best-image",
                 "hello",
                 "world",
-            ]
+            ],
+            env={"DOCKER_CLI_HINTS": "false"},
         ),
     )
 ]
diff --git a/tests/integrations/docker/test_Docker__subprocess_env.py b/tests/integrations/docker/test_Docker__subprocess_env.py
new file mode 100644
index 00000000..f9f47d1d
--- /dev/null
+++ b/tests/integrations/docker/test_Docker__subprocess_env.py
@@ -0,0 +1,22 @@
+import pytest
+
+
+@pytest.mark.parametrize(
+    "env, subprocess_env",
+    [
+        (None, {"DOCKER_CLI_HINTS": "false"}),
+        ({}, {"DOCKER_CLI_HINTS": "false"}),
+        ({"ENV_VAR": "VALUE"}, {"ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "false"}),
+        (
+            {"ENV_VAR": "VALUE", "DOCKER_CLI_HINTS": "true"},
+            {
+                "ENV_VAR": "VALUE",
+                "DOCKER_CLI_HINTS": "true",
+            },
+        ),
+    ],
+)
+@pytest.mark.usefixtures("mock_docker")
+def test_subprocess_env(mock_tools, env, subprocess_env):
+    """The env for subprocess calls is set properly."""
+    assert mock_tools.docker.subprocess_env(env) == subprocess_env
diff --git a/tests/integrations/docker/test_Docker__verify.py b/tests/integrations/docker/test_Docker__verify.py
index f8b864c6..ad65382a 100644
--- a/tests/integrations/docker/test_Docker__verify.py
+++ b/tests/integrations/docker/test_Docker__verify.py
@@ -16,9 +16,9 @@ VALID_DOCKER_INFO = "docker info printout"
 VALID_BUILDX_VERSION = "github.com/docker/buildx v0.10.2 00ed17d\n"
 VALID_USER_MAPPING_IMAGE_CACHE = "1ed313b0551f"
 DOCKER_VERIFICATION_CALLS = [
-    call(["docker", "--version"]),
-    call(["docker", "info"]),
-    call(["docker", "buildx", "version"]),
+    call(["docker", "--version"], env={"DOCKER_CLI_HINTS": "false"}),
+    call(["docker", "info"], env={"DOCKER_CLI_HINTS": "false"}),
+    call(["docker", "buildx", "version"], env={"DOCKER_CLI_HINTS": "false"}),
 ]
 
 
@@ -104,7 +104,9 @@ def test_docker_doesnt_exist(mock_tools):
         Docker.verify(mock_tools)
 
     # But docker was invoked
-    mock_tools.subprocess.check_output.assert_called_with(["docker", "--version"])
+    mock_tools.subprocess.check_output.assert_called_with(
+        ["docker", "--version"], env={"DOCKER_CLI_HINTS": "false"}
+    )
 
 
 def test_docker_failure(mock_tools, user_mapping_run_calls, capsys):
@@ -295,7 +297,10 @@ def test_docker_image_hint(mock_tools):
     mock_tools.subprocess.check_output.assert_has_calls(
         DOCKER_VERIFICATION_CALLS
         + [
-            call(["docker", "images", "-q", "myimage:tagtorulethemall"]),
+            call(
+                ["docker", "images", "-q", "myimage:tagtorulethemall"],
+                env={"DOCKER_CLI_HINTS": "false"},
+            ),
             call(
                 args=[
                     "docker",
@@ -307,6 +312,7 @@ def test_docker_image_hint(mock_tools):
                     "touch",
                     "/host_write_test/container_write_test",
                 ],
+                env={"DOCKER_CLI_HINTS": "false"},
             ),
             call(
                 args=[
@@ -320,6 +326,7 @@ def test_docker_image_hint(mock_tools):
                     "-f",
                     "/host_write_test/container_write_test",
                 ],
+                env={"DOCKER_CLI_HINTS": "false"},
             ),
         ]
     )
diff --git a/tests/platforms/linux/appimage/test_build.py b/tests/platforms/linux/appimage/test_build.py
index b2939d33..3143b99e 100644
--- a/tests/platforms/linux/appimage/test_build.py
+++ b/tests/platforms/linux/appimage/test_build.py
@@ -446,6 +446,10 @@ def test_build_appimage_in_docker(
             "--deploy-deps-only",
             "/app/First App.AppDir/usr/app_packages/secondlib",
         ],
+        env={
+            "PATH": "/usr/local/bin:/usr/bin:/path/to/somewhere",
+            "DOCKER_CLI_HINTS": "false",
+        },
         **sub_stream_kw,
     )
     # Binary is marked executable
@@ -580,6 +584,10 @@ def test_build_appimage_with_plugins_in_docker(
             "--plugin",
             "something",
         ],
+        env={
+            "PATH": "/usr/local/bin:/usr/bin:/path/to/somewhere",
+            "DOCKER_CLI_HINTS": "false",
+        },
         **sub_stream_kw,
     )
     # Local plugin marked executable
diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py
index 73d5c646..843c206d 100644
--- a/tests/platforms/linux/system/test_run.py
+++ b/tests/platforms/linux/system/test_run.py
@@ -178,6 +178,11 @@ def test_supported_host_os_docker(
         return_value=log_popen
     )
 
+    # Mock out the environment
+    monkeypatch.setattr(
+        run_command.tools.os, "environ", {"ENVVAR": "Value", "DISPLAY": ":99"}
+    )
+
     # Parse the command line
     run_command.parse_options([])
 
@@ -210,6 +215,7 @@ def test_supported_host_os_docker(
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        env={"ENVVAR": "Value", "DISPLAY": ":99", "DOCKER_CLI_HINTS": "false"},
         **sub_kw,
     )
 
@@ -270,7 +276,7 @@ def test_run_app(run_command, first_app, sub_kw, tmp_path):
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="Windows paths can't be dockerized")
-def test_run_app_docker(run_command, first_app, sub_kw, tmp_path):
+def test_run_app_docker(run_command, first_app, sub_kw, tmp_path, monkeypatch):
     """A bootstrap binary can be started in Docker."""
     # Trigger to run in Docker
     run_command.target_image = first_app.target_image = "best/distro"
@@ -286,6 +292,11 @@ def test_run_app_docker(run_command, first_app, sub_kw, tmp_path):
         return_value=log_popen
     )
 
+    # Mock out the environment
+    monkeypatch.setattr(
+        run_command.tools.os, "environ", {"ENVVAR": "Value", "DISPLAY": ":99"}
+    )
+
     # Run the app
     run_command.run_app(first_app, test_mode=False, passthrough=[])
 
@@ -315,6 +326,7 @@ def test_run_app_docker(run_command, first_app, sub_kw, tmp_path):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        env={"ENVVAR": "Value", "DISPLAY": ":99", "DOCKER_CLI_HINTS": "false"},
         **sub_kw,
     )
 
@@ -358,7 +370,7 @@ def test_run_app_failed(run_command, first_app, sub_kw, tmp_path):
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="Windows paths can't be dockerized")
-def test_run_app_failed_docker(run_command, first_app, sub_kw, tmp_path):
+def test_run_app_failed_docker(run_command, first_app, sub_kw, tmp_path, monkeypatch):
     """If there's a problem starting the app in Docker, an exception is raised."""
 
     # Trigger to run in Docker
@@ -369,6 +381,11 @@ def test_run_app_failed_docker(run_command, first_app, sub_kw, tmp_path):
     # Provide Docker app context
     run_command.verify_app_tools(app=first_app)
 
+    # Mock out the environment
+    monkeypatch.setattr(
+        run_command.tools.os, "environ", {"ENVVAR": "Value", "DISPLAY": ":99"}
+    )
+
     run_command.tools.subprocess._subprocess.Popen.side_effect = OSError
 
     with pytest.raises(OSError):
@@ -400,6 +417,7 @@ def test_run_app_failed_docker(run_command, first_app, sub_kw, tmp_path):
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        env={"ENVVAR": "Value", "DISPLAY": ":99", "DOCKER_CLI_HINTS": "false"},
         **sub_kw,
     )
 
@@ -470,6 +488,11 @@ def test_run_app_test_mode_docker(
     log_popen = mock.MagicMock()
     run_command.tools.subprocess._subprocess.Popen.return_value = log_popen
 
+    # Mock out the environment
+    monkeypatch.setattr(
+        run_command.tools.os, "environ", {"ENVVAR": "Value", "DISPLAY": ":99"}
+    )
+
     # Run the app
     run_command.run_app(first_app, test_mode=True, passthrough=[])
 
@@ -501,6 +524,7 @@ def test_run_app_test_mode_docker(
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        env={"ENVVAR": "Value", "DISPLAY": ":99", "DOCKER_CLI_HINTS": "false"},
         **sub_kw,
     )
 
@@ -588,6 +612,11 @@ def test_run_app_test_mode_with_args_docker(
     log_popen = mock.MagicMock()
     run_command.tools.subprocess._subprocess.Popen.return_value = log_popen
 
+    # Mock out the environment
+    monkeypatch.setattr(
+        run_command.tools.os, "environ", {"ENVVAR": "Value", "DISPLAY": ":99"}
+    )
+
     # Run the app with args
     run_command.run_app(
         first_app,
@@ -625,6 +654,7 @@ def test_run_app_test_mode_with_args_docker(
         stdout=subprocess.PIPE,
         stderr=subprocess.STDOUT,
         bufsize=1,
+        env={"ENVVAR": "Value", "DISPLAY": ":99", "DOCKER_CLI_HINTS": "false"},
         **sub_kw,
     )
 
diff --git a/tests/platforms/linux/test_DockerOpenCommand.py b/tests/platforms/linux/test_DockerOpenCommand.py
index 4dbb413f..56e41108 100644
--- a/tests/platforms/linux/test_DockerOpenCommand.py
+++ b/tests/platforms/linux/test_DockerOpenCommand.py
@@ -105,6 +105,7 @@ def test_open_docker(open_command, first_app_config, tmp_path, monkeypatch):
             f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
         ],
         stream_output=False,
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
 
diff --git a/tests/platforms/linux/test_LocalRequirementsMixin.py b/tests/platforms/linux/test_LocalRequirementsMixin.py
index 95bc089f..2792867c 100644
--- a/tests/platforms/linux/test_LocalRequirementsMixin.py
+++ b/tests/platforms/linux/test_LocalRequirementsMixin.py
@@ -194,6 +194,7 @@ def test_install_app_requirements_in_docker(create_command, first_app_config, tm
         ],
         check=True,
         encoding="UTF-8",
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
     # The local requirements path exists, but is empty
@@ -374,6 +375,7 @@ def test_install_app_requirements_with_locals(
         ],
         check=True,
         encoding="UTF-8",
+        env={"DOCKER_CLI_HINTS": "false"},
     )
 
     # The local requirements path exists, and contains the compiled sdist, the