Skip to content

MCP gateway can't reach the Docker daemon on split-daemon topologies (ARC/DinD, DOCKER_HOST=tcp://): socket-GID resolution assumes a local /var/run/docker.sock #44251

Description

@github-antoine-brechon

Summary

The MCP gateway is launched as a non-root container (--user <uid>:<gid>) that reads the Docker socket via --group-add <gid>, where <gid> is computed by stat-ing a socket path on the launcher (runner) host. For any non-unix DOCKER_HOST (tcp://, ssh://, npipe://) the path falls back to /var/run/docker.sock. On split-daemon / remote-daemon topologies — notably ARC runners with a Docker-in-Docker sidecar, where DOCKER_HOST=tcp://… and the daemon's unix socket lives in the sidecar, not on the runner — that path doesn't exist on the launcher host. The stat fails, --group-add silently falls back to 0, and the non-root gateway cannot access the socket:

[ERROR] Docker daemon is not accessible

Operators currently have to carry a socket-symlink + chown workaround in their runner/pod spec to satisfy this. This issue proposes making gh-aw resolve the socket group robustly (and fail loudly), so no host-side hack is needed.

Environment

  • gh-aw v0.82.4 (behavior present since ~v0.80.9 / mcpg v0.3.27)
  • Runner: actions-runner-controller gha-runner-scale-set with a privileged dind sidecar; runner: topology: arc-dind
  • DOCKER_HOST=tcp://127.0.0.1:2375 on the runner (daemon in the sidecar); the daemon's unix socket is exposed to the runner via a shared volume at a non-default path, group-owned by the docker group
  • Any engine using the MCP gateway (github / safeoutputs MCP)

Symptom

The gateway container starts as --user 1001:1001 --group-add 0 and cannot read the bind-mounted socket → Docker daemon is not accessible, and MCP tools never come up. On our setup this also intersects with the tcp-only daemon (there is no /var/run/docker.sock on the runner at all).

Root cause (source analysis — pkg/workflow/mcp_setup_generator.go)

generateMCPGatewaySetup emits this shell into the "Start MCP Gateway" step:

MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0')
MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0')
case "${DOCKER_HOST:-}" in
  unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;;
  /* )       DOCKER_SOCK_PATH="$DOCKER_HOST" ;;
  * )        DOCKER_SOCK_PATH=/var/run/docker.sock ;;   # tcp://, ssh://, npipe:// all land here
esac
DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0')

buildMCPGatewayContainerCommand then emits:

--user ${MCP_GATEWAY_UID}:${MCP_GATEWAY_GID}
--group-add ${DOCKER_SOCK_GID}
-v ${DOCKER_SOCK_PATH}:/var/run/docker.sock

and appendMCPGatewayBaseEnvFlags forces:

-e DOCKER_HOST=unix:///var/run/docker.sock

Three problems compound on a split/remote daemon:

  1. Wrong socket path for non-unix DOCKER_HOST. With DOCKER_HOST=tcp://…, DOCKER_SOCK_PATH falls back to /var/run/docker.sock, a launcher-host path that need not exist (the daemon is remote / in a sidecar). Both the stat (for the GID) and the -v …:/var/run/docker.sock bind-mount then target a nonexistent path (Docker silently creates an empty directory at the mount source).
  2. Silent fallback to group 0. stat … 2>/dev/null || echo '0' yields DOCKER_SOCK_GID=0 on failure. Combined with --user <uid>:<gid> (non-root), the gateway is added only to group 0 (root) and cannot read a socket owned by the docker group. The failure surfaces later as a misleading Docker daemon is not accessible rather than "couldn't determine the Docker socket group."
  3. stat (no -L) reads the path's own group. Even where operators expose the socket via a symlink at /var/run/docker.sock, stat -c '%g' (without -L) reports the symlink's own group, not the resolved socket's — so simply symlinking is insufficient.

The workaround operators must carry today

To satisfy the generated logic, the runner/pod spec has to do:

# make the stat'd path exist and resolve (for the -v bind-mount) to the real sidecar socket
sudo ln -sf /dind-sock/docker.sock /var/run/docker.sock
# give the symlink ITSELF the docker gid (123) so `stat -c %g` (no -L) returns 123, not 0
sudo chown -h root:docker /var/run/docker.sock

This is fragile: it hard-codes the fallback path /var/run/docker.sock, depends on stat reading the link's own gid (hence chown -h), and assumes the docker group id (123). It should not be necessary.

Reproduction

  1. Run the agent (or any MCP-using workflow) on a runner where DOCKER_HOST=tcp://… and there is no /var/run/docker.sock on the launcher host (e.g. ARC + dind sidecar).
  2. Observe the gateway start with --group-add 0 and fail with Docker daemon is not accessible; MCP tools never initialize.

Proposed fix + implementation plan

All changes in pkg/workflow/mcp_setup_generator.go unless noted.

  1. Honor explicit overrides (recommended, minimal). Before the DOCKER_HOST-derived guess, respect operator-provided values for the socket path and group — e.g. environment variables GH_AW_DOCKER_SOCK_PATH and GH_AW_DOCKER_SOCK_GID (or fields on the runner/awf config):

    DOCKER_SOCK_PATH="${GH_AW_DOCKER_SOCK_PATH:-$derived_path}"
    DOCKER_SOCK_GID="${GH_AW_DOCKER_SOCK_GID:-$(stat -Lc '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '')}"

    This lets split-daemon operators point the gateway at the real socket + group directly, removing the symlink/chown hack entirely.

  2. Follow symlinks when stat-ing — use stat -Lc '%g' so a symlinked socket resolves to the real socket's group (drops the need for chown -h).

  3. Fail loudly instead of || echo '0'. When the socket group cannot be resolved and no override is given, error with an actionable message (name the path tried and the override env var) rather than proceeding with --group-add 0, which guarantees a confusing downstream Docker daemon is not accessible.

  4. (Ideal, larger) Talk to the daemon directly when DOCKER_HOST is tcp://. When the daemon is reachable over TCP (or runner.topology: arc-dind, which gh-aw already knows), pass the original tcp:// DOCKER_HOST into the gateway container and skip the unix-socket bind-mount, the --group-add, and the -e DOCKER_HOST=unix://… override (buildMCPGatewayContainerCommand / appendMCPGatewayBaseEnvFlags). This removes the socket-group problem for the entire class of tcp/split-daemon topologies. Note the gateway runs in a bridge network, so reaching the host's tcp endpoint needs host.docker.internal:host-gateway (already added for other cases) or equivalent — gate this path so unix-socket setups are unchanged.

  5. Tests (pkg/workflow/mcp_setup_generator_test.go): assert that with DOCKER_HOST=tcp://… (and/or runner.topology: arc-dind) the generated gateway command either honors the override / resolves a non-zero group, or (for option 4) connects over tcp with no --group-add; and that an unresolved group fails the step rather than emitting --group-add 0.

  6. Docs: document the override(s) and the ARC/DinD / remote-daemon guidance in the MCP gateway / runner-topology reference.

  7. Run make agent-finish (build, test, recompile, format, lint) before completing.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions