fix(core): Fix retrieval of the docker socket path when using rootless docker
Currently the docker socket path used by ryuk is not working for rootless docker. We need to manually set it through the TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable.
When running locally the DOCKER_HOST is always the full URL to the docker socket, e.g. on linux/macos, it's the socket path prefixed with unix:// and npipe:// on windows. So we can easily extract the socket path using urllib parse:
from urllib.parse import urlparse
docker_socket = urlparse(DOCKER_HOST).path
I improved the TestcontainersConfiguration so that the docker socket is inferred from the docker host when defined (and when the RYUK_DOCKER_SOCKET env is not explicitly defined)
The advantage of this approach is that it is automatically inferred from the DOCKER_HOST most of the time without the need for tedious manual configuration. And users can still explicitly define it for edge use-cases like before (e.g. when using docker over the network)
I moved get_docker_host() to make it part of the TestcontainerConfig, it was used at only 2 places, so it did not required many changes. And it makes sense to be part of the config
Alternative solution
An alternatively solution has been proposed in https://github.com/testcontainers/testcontainers-python/issues/537. But it would rely on many uncertain and moving parts:
- the path to the docker socket in rootless docker to always look like this
"/run/user/{user_id}/docker.sock"which might change in the future or be different depending on the OS and config - being able to get the user ID somehow, on linux we can do it through the
UIDenv variable, not sure if this works on macos, and it would probably not work on windows.
This solution could be implemented in container.py, e.g.:
def _create_instance(cls) -> "Reaper":
logger.debug(f"Creating new Reaper for session: {SESSION_ID}")
client = DockerClient()
info = client.info()
sec_opts = info.get('SecurityOptions') or tuple()
is_rootless = any('rootless' in s for s in sec_opts)
docker_socket = c.ryuk_docker_socket
user_id = os.getenv('UID')
if is_rootless and user_id:
docker_socket = f"/run/user/{user_id}/docker.sock"
Reaper._container = (
DockerContainer(c.ryuk_image)
.with_name(f"testcontainers-ryuk-{SESSION_ID}")
.with_exposed_ports(8080)
.with_volume_mapping(docker_socket, "/var/run/docker.sock", "rw")
.with_kwargs(privileged=c.ryuk_privileged, auto_remove=True)
.with_env("RYUK_RECONNECTION_TIMEOUT", c.ryuk_reconnection_timeout)
.start()
)
And would require to add client.info() in docker_client.py
def info(self) -> Any:
return self.client.info()
Fixes https://github.com/testcontainers/testcontainers-python/issues/537
@alexanderankin
"And would require to add client.info() in docker_client.py"
just add a TypedDict or something so its not just totally exposing another library API and I think that should be ok
will take a look time permitting
cc @kiview
import docker
client = docker.from_env()
print(client.api.get_adapter(client.api.base_url).socket_path)
Could be used to detect the correct socket_path.
If socket_path is not defined, just use the default.
It think the solution here is compare to this much overengineered.
See https://github.com/testcontainers/testcontainers-python/pull/779 for an alternative solution, that does not require the creation of a /.testcontainers.properties but just works out of the box.
closed in favor of #779