DEVX-155: fix: prefer rootless Docker socket over low-space inner DinD daemon
Post-merge / detect-and-configure (push) Canceled after 0s
Post-merge / release-and-maintain (push) Canceled after 0s

When a CI container has an inner dockerd (DinD) writing to the
container's overlay (e.g. 38 GB), image pulls fail with ENOSPC.
start_docker.py now checks the daemon's free disk space and tries
rootless sockets (which have access to the host's full filesystem)
when the default socket has less than 20 GB free.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
gitea-actions-bot
2026-08-14 17:27:58 +02:00
committed by emil
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 0341ee74c9
commit 23bd480e80
2 changed files with 188 additions and 48 deletions
+93 -18
View File
@@ -10,6 +10,12 @@ If the host socket is not available, it tries the rootless socket, then
starts a local ``dockerd`` with the vfs storage driver (requires
privileged container).
When the host socket IS available but has limited disk space (e.g. an
inner DinD daemon writing to a 38 GB container overlay), the script
prefers a rootless socket that has more available space. This prevents
"no space left on device" errors during molecule tests that pull images
and create containers via the Docker daemon.
Usage::
python3 -m devx.molecule.start_docker [--timeout 30]
@@ -19,6 +25,7 @@ from __future__ import annotations
import glob
import os
import shutil
import subprocess # nosec B404
import sys
import tempfile
@@ -32,6 +39,10 @@ DEFAULT_TIMEOUT = 30
DOCKER_SOCK = "/var/run/docker.sock"
# Rootless socket fallback (e.g. /run/user/994/docker.sock)
ROOTLESS_SOCK = f"/run/user/{os.getuid()}/docker.sock"
# Minimum free bytes for a Docker daemon to be considered usable.
# Below this, image pulls and container creation will fail with ENOSPC.
# 20 GB leaves room for molecule-test-base (~500 MB) + a few containers.
MIN_FREE_BYTES = 20 * 1024**3 # 20 GB
def is_docker_ready() -> bool:
@@ -46,6 +57,46 @@ def is_docker_ready() -> bool:
return result.returncode == 0
def _get_docker_free_bytes() -> int:
"""Get free disk space (bytes) at the Docker daemon's data root.
Returns 0 if the daemon is not reachable or the data root cannot be
determined.
"""
docker_host = os.environ.get("DOCKER_HOST", f"unix://{DOCKER_SOCK}")
try:
result = subprocess.run( # nosec B603 B607
[
"docker",
"info",
"--format",
"{{.DockerRootDir}}",
],
capture_output=True,
text=True,
timeout=10,
check=False,
env={**os.environ, "DOCKER_HOST": docker_host},
)
if result.returncode != 0 or not result.stdout.strip():
return 0
data_root = result.stdout.strip()
if not os.path.exists(data_root):
return 0
return shutil.disk_usage(data_root).free
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
return 0
def _try_socket(sock_path: str) -> bool:
"""Set DOCKER_HOST to *sock_path* and check if the daemon is ready.
Returns ``True`` if the daemon responds, ``False`` otherwise.
"""
os.environ["DOCKER_HOST"] = f"unix://{sock_path}"
return is_docker_ready()
def _diagnose_socket() -> None:
"""Print diagnostic info about the Docker socket."""
click.echo(f"DOCKER_HOST = {os.environ.get('DOCKER_HOST', '(not set)')}")
@@ -97,10 +148,14 @@ def _diagnose_socket() -> None:
def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
"""Ensure Docker is ready for molecule tests.
First tries the host socket. If that works, sets ``DOCKER_HOST`` and
returns immediately. If not, tries the rootless socket. If neither
works, starts a local ``dockerd`` with vfs storage driver (requires
privileged container).
Tries sockets in this order, preferring ones with enough disk space:
1. Host socket (``/var/run/docker.sock``) — if it has ≥ ``MIN_FREE_BYTES``
free space, use it immediately.
2. Rootless sockets (``/run/user/*/docker.sock``) — if the host socket
has insufficient space, try rootless sockets which may have access
to the host's full filesystem.
3. Local ``dockerd`` with vfs storage driver — last resort.
Returns ``True`` if Docker is ready, ``False`` if it failed to
start within the timeout.
@@ -115,25 +170,45 @@ def start_docker_daemon(timeout: int = DEFAULT_TIMEOUT) -> bool:
# Check if host Docker is already available
if is_docker_ready():
click.echo(_("Docker daemon already running"))
return True
free_bytes = _get_docker_free_bytes()
free_gb = free_bytes / 1024**3
click.echo(f"Docker daemon already running (free space: {free_gb:.1f} GB)")
if free_bytes >= MIN_FREE_BYTES:
return True
click.echo(
f"Host Docker has only {free_gb:.1f} GB free "
f"(need ≥ {MIN_FREE_BYTES / 1024**3:.0f} GB), trying rootless sockets..."
)
else:
click.echo("Host Docker not available, trying rootless sockets...")
# Try rootless socket (e.g. /run/user/994/docker.sock)
click.echo(f"Trying rootless socket: {ROOTLESS_SOCK}")
os.environ["DOCKER_HOST"] = f"unix://{ROOTLESS_SOCK}"
if os.path.exists(ROOTLESS_SOCK) and is_docker_ready():
click.echo(_("Docker daemon already running"))
return True
# Scan for any rootless sockets at other UIDs
# Collect candidate rootless sockets
candidates: list[str] = []
if os.path.exists(ROOTLESS_SOCK):
candidates.append(ROOTLESS_SOCK)
for sock in sorted(glob.glob("/run/user/*/docker.sock")):
if sock == ROOTLESS_SOCK:
if sock not in candidates:
candidates.append(sock)
# Try each rootless socket — prefer one with enough free space
for sock in candidates:
click.echo(f"Trying rootless socket: {sock}")
if not _try_socket(sock):
continue
click.echo(f"Trying alternative rootless socket: {sock}")
os.environ["DOCKER_HOST"] = f"unix://{sock}"
if is_docker_ready():
free_bytes = _get_docker_free_bytes()
free_gb = free_bytes / 1024**3
click.echo(f" Docker daemon ready (free space: {free_gb:.1f} GB)")
if free_bytes >= MIN_FREE_BYTES:
click.echo(_("Docker daemon already running"))
return True
click.echo(f" Insufficient space ({free_gb:.1f} GB), trying next...")
# If we found a working rootless socket but with low space, use it
# as a fallback (better than nothing).
for sock in reversed(candidates):
if _try_socket(sock):
click.echo(f"Using low-space fallback: {sock}")
return True
click.echo(_("Host Docker not available, starting local dockerd..."))