From f663aade534b4350401d8438cca1e2cabcbbd477 Mon Sep 17 00:00:00 2001 From: Surendra Raika Date: Wed, 18 Feb 2026 19:23:14 +0530 Subject: [PATCH] feat(docker): add macOS Docker Desktop socket auto-detection Probe ~/.docker/run/docker.sock for RuntimeDocker and RuntimeAuto before falling back to /var/run/docker.sock. This lets the agent connect on macOS without requiring DOCKER_HOST to be set manually. Ref #1200 --- docs/DOCKER.md | 12 ++++ internal/dockeragent/agent.go | 9 +++ internal/dockeragent/agent_internal_test.go | 66 ++++++++++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/docs/DOCKER.md b/docs/DOCKER.md index 535a0d99c..b93ed5d17 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -180,6 +180,18 @@ You can also toggle "Hide Docker Update Buttons" from the UI: **Settings → Age --- +## 🍎 macOS Docker Desktop Socket + +On macOS, Docker Desktop exposes a socket at `~/.docker/run/docker.sock`. Pulse auto-detects this path when `--enable-docker` is used with `RuntimeDocker` or `RuntimeAuto`, so no extra configuration is needed. + +To override, set `DOCKER_HOST`: + +```bash +export DOCKER_HOST=unix://$HOME/.docker/run/docker.sock +``` + +--- + ## 🛠️ Troubleshooting - **Forgot Password?** diff --git a/internal/dockeragent/agent.go b/internal/dockeragent/agent.go index 9c3703866..acaf56ad0 100644 --- a/internal/dockeragent/agent.go +++ b/internal/dockeragent/agent.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "os/exec" + "path/filepath" "strings" "sync" "time" @@ -508,6 +509,14 @@ func buildRuntimeCandidates(preference RuntimeKind) []runtimeCandidate { label: "docker rootless socket", }) + // macOS Docker Desktop socket + if home := os.Getenv("HOME"); home != "" { + add(runtimeCandidate{ + host: "unix://" + filepath.Join(home, ".docker", "run", "docker.sock"), + label: "docker desktop socket", + }) + } + add(runtimeCandidate{ host: "unix:///var/run/docker.sock", label: "default docker socket", diff --git a/internal/dockeragent/agent_internal_test.go b/internal/dockeragent/agent_internal_test.go index 1a5875eba..c13f5ec6d 100644 --- a/internal/dockeragent/agent_internal_test.go +++ b/internal/dockeragent/agent_internal_test.go @@ -927,12 +927,12 @@ func TestBuildRuntimeCandidates(t *testing.T) { { name: "auto includes both podman and docker sockets", preference: RuntimeAuto, - wantMin: 3, // at least env defaults + podman rootless + docker default + wantMin: 4, // at least env defaults + podman rootless + docker rootless + docker desktop + docker default }, { name: "docker preference includes docker socket", preference: RuntimeDocker, - wantMin: 2, // at least env defaults + docker default + wantMin: 3, // at least env defaults + docker rootless + docker desktop + docker default }, { name: "podman preference includes podman sockets", @@ -1052,6 +1052,68 @@ func TestBuildRuntimeCandidatesDockerRootlessOrder(t *testing.T) { }) } +func TestBuildRuntimeCandidatesDockerDesktop(t *testing.T) { + t.Run("docker includes docker desktop socket", func(t *testing.T) { + candidates := buildRuntimeCandidates(RuntimeDocker) + found := false + for _, c := range candidates { + if c.label == "docker desktop socket" { + found = true + break + } + } + if !found { + t.Error("docker preference should include docker desktop socket") + } + }) + + t.Run("auto includes docker desktop socket", func(t *testing.T) { + candidates := buildRuntimeCandidates(RuntimeAuto) + found := false + for _, c := range candidates { + if c.label == "docker desktop socket" { + found = true + break + } + } + if !found { + t.Error("auto preference should include docker desktop socket") + } + }) + + t.Run("podman excludes docker desktop socket", func(t *testing.T) { + candidates := buildRuntimeCandidates(RuntimePodman) + for _, c := range candidates { + if c.label == "docker desktop socket" { + t.Error("podman preference should not include docker desktop socket") + } + } + }) + + t.Run("desktop socket comes before default socket", func(t *testing.T) { + candidates := buildRuntimeCandidates(RuntimeDocker) + desktopIdx := -1 + defaultIdx := -1 + for i, c := range candidates { + if c.label == "docker desktop socket" && desktopIdx < 0 { + desktopIdx = i + } + if c.label == "default docker socket" && defaultIdx < 0 { + defaultIdx = i + } + } + if desktopIdx < 0 { + t.Fatal("expected docker desktop socket candidate") + } + if defaultIdx < 0 { + t.Fatal("expected default docker socket candidate") + } + if desktopIdx > defaultIdx { + t.Fatalf("expected docker desktop socket before default; desktopIdx=%d defaultIdx=%d", desktopIdx, defaultIdx) + } + }) +} + func TestBuildRuntimeCandidatesEnv(t *testing.T) { t.Setenv("DOCKER_HOST", "unix:///tmp/docker.sock") t.Setenv("CONTAINER_HOST", "unix:///tmp/container.sock")