From 4853672a7dc2e5d636887cb8cf308af2e73ade08 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Fri, 20 Mar 2026 15:53:45 +0200 Subject: [PATCH 1/5] Skip Docker socket bind for Colima and OrbStack --- internal/runtime/docker.go | 14 +++++++++++++- internal/runtime/docker_test.go | 25 ++++++++++++++++++++----- test/integration/start_test.go | 6 ++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index d14ba5d..9706987 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -71,10 +71,22 @@ func probeSocket(candidates ...string) string { return "" } +// SocketPath returns the Unix socket path used by the Docker client. +// Returns the actual path for the standard /var/run/docker.sock so callers +// can bind-mount it into containers. Returns empty string for non-standard +// sockets (Colima, OrbStack) which communicate with VMs and cannot be +// bind-mounted. Called by internal/container/start.go to decide whether +// to add a Docker socket bind-mount when starting LocalStack. func (d *DockerRuntime) SocketPath() string { host := d.client.DaemonHost() if strings.HasPrefix(host, "unix://") { - return strings.TrimPrefix(host, "unix://") + sock := strings.TrimPrefix(host, "unix://") + // Skip bind-mount for non-standard sockets (Colima, OrbStack, etc.) + // These sockets communicate with VMs and cannot be bind-mounted + if sock != "/var/run/docker.sock" { + return "" + } + return sock } return "" } diff --git a/internal/runtime/docker_test.go b/internal/runtime/docker_test.go index a77c004..caab79f 100644 --- a/internal/runtime/docker_test.go +++ b/internal/runtime/docker_test.go @@ -41,11 +41,26 @@ func TestProbeSocket_ReturnsEmptyForNoCandidates(t *testing.T) { } func TestSocketPath_ExtractsUnixPath(t *testing.T) { - cli, err := client.NewClientWithOpts(client.WithHost("unix:///home/user/.colima/default/docker.sock")) - require.NoError(t, err) - rt := &DockerRuntime{client: cli} - - assert.Equal(t, "/home/user/.colima/default/docker.sock", rt.SocketPath()) + t.Run("standard socket returns path", func(t *testing.T) { + cli, err := client.NewClientWithOpts(client.WithHost("unix:///var/run/docker.sock")) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + }) + + t.Run("non-standard socket returns empty", func(t *testing.T) { + cli, err := client.NewClientWithOpts(client.WithHost("unix:///home/user/.colima/default/docker.sock")) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "", rt.SocketPath(), "non-standard sockets should return empty to skip bind-mount") + }) + + t.Run("orbstack socket returns empty", func(t *testing.T) { + cli, err := client.NewClientWithOpts(client.WithHost("unix:///Users/user/.orbstack/run/docker.sock")) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "", rt.SocketPath(), "non-standard sockets should return empty to skip bind-mount") + }) } func TestSocketPath_ReturnsEmptyForTCPHost(t *testing.T) { diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 2e98d5d..3d6830e 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -170,6 +170,12 @@ func TestStartCommandSetsUpContainerCorrectly(t *testing.T) { t.Skip("Docker daemon is not reachable via unix socket") } + // Skip bind-mount assertion for non-standard sockets (Colima, OrbStack) + // since these communicate with VMs and cannot be bind-mounted + if dockerClient.DaemonHost() != "unix:///var/run/docker.sock" { + t.Skip("Docker daemon uses non-standard socket (Colima/OrbStack) - socket not bind-mounted") + } + assert.True(t, hasBindTarget(inspect.HostConfig.Binds, "/var/run/docker.sock"), "expected Docker socket bind mount to /var/run/docker.sock, got: %v", inspect.HostConfig.Binds) From d0cf62c1d3a41d29444952aa3478f16206abdc51 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Fri, 20 Mar 2026 17:51:59 +0200 Subject: [PATCH 2/5] Mount daemon Docker socket for nested workloads --- internal/runtime/docker.go | 19 ++++++------------- internal/runtime/docker_test.go | 10 +++++----- test/integration/start_test.go | 18 ++++++++++++------ 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index 9706987..2a2c506 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -71,22 +71,15 @@ func probeSocket(candidates ...string) string { return "" } -// SocketPath returns the Unix socket path used by the Docker client. -// Returns the actual path for the standard /var/run/docker.sock so callers -// can bind-mount it into containers. Returns empty string for non-standard -// sockets (Colima, OrbStack) which communicate with VMs and cannot be -// bind-mounted. Called by internal/container/start.go to decide whether -// to add a Docker socket bind-mount when starting LocalStack. +// SocketPath returns the daemon-visible Unix socket path to bind-mount into +// containers so LocalStack can launch nested workloads such as Lambda functions. +// Even when the CLI connects through a user-scoped socket (for example Colima), +// the daemon resolves bind mounts inside its own environment where the socket is +// exposed at /var/run/docker.sock. func (d *DockerRuntime) SocketPath() string { host := d.client.DaemonHost() if strings.HasPrefix(host, "unix://") { - sock := strings.TrimPrefix(host, "unix://") - // Skip bind-mount for non-standard sockets (Colima, OrbStack, etc.) - // These sockets communicate with VMs and cannot be bind-mounted - if sock != "/var/run/docker.sock" { - return "" - } - return sock + return "/var/run/docker.sock" } return "" } diff --git a/internal/runtime/docker_test.go b/internal/runtime/docker_test.go index caab79f..663a85d 100644 --- a/internal/runtime/docker_test.go +++ b/internal/runtime/docker_test.go @@ -41,25 +41,25 @@ func TestProbeSocket_ReturnsEmptyForNoCandidates(t *testing.T) { } func TestSocketPath_ExtractsUnixPath(t *testing.T) { - t.Run("standard socket returns path", func(t *testing.T) { + t.Run("standard socket returns daemon path", func(t *testing.T) { cli, err := client.NewClientWithOpts(client.WithHost("unix:///var/run/docker.sock")) require.NoError(t, err) rt := &DockerRuntime{client: cli} assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) }) - t.Run("non-standard socket returns empty", func(t *testing.T) { + t.Run("non-standard socket returns daemon path", func(t *testing.T) { cli, err := client.NewClientWithOpts(client.WithHost("unix:///home/user/.colima/default/docker.sock")) require.NoError(t, err) rt := &DockerRuntime{client: cli} - assert.Equal(t, "", rt.SocketPath(), "non-standard sockets should return empty to skip bind-mount") + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) }) - t.Run("orbstack socket returns empty", func(t *testing.T) { + t.Run("orbstack socket returns daemon path", func(t *testing.T) { cli, err := client.NewClientWithOpts(client.WithHost("unix:///Users/user/.orbstack/run/docker.sock")) require.NoError(t, err) rt := &DockerRuntime{client: cli} - assert.Equal(t, "", rt.SocketPath(), "non-standard sockets should return empty to skip bind-mount") + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) }) } diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 3d6830e..04f3b06 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -170,14 +170,10 @@ func TestStartCommandSetsUpContainerCorrectly(t *testing.T) { t.Skip("Docker daemon is not reachable via unix socket") } - // Skip bind-mount assertion for non-standard sockets (Colima, OrbStack) - // since these communicate with VMs and cannot be bind-mounted - if dockerClient.DaemonHost() != "unix:///var/run/docker.sock" { - t.Skip("Docker daemon uses non-standard socket (Colima/OrbStack) - socket not bind-mounted") - } - assert.True(t, hasBindTarget(inspect.HostConfig.Binds, "/var/run/docker.sock"), "expected Docker socket bind mount to /var/run/docker.sock, got: %v", inspect.HostConfig.Binds) + assert.True(t, hasBindSource(inspect.HostConfig.Binds, "/var/run/docker.sock"), + "expected Docker socket bind mount from /var/run/docker.sock, got: %v", inspect.HostConfig.Binds) envVars := containerEnvToMap(inspect.Config.Env) assert.Equal(t, "unix:///var/run/docker.sock", envVars["DOCKER_HOST"]) @@ -253,6 +249,16 @@ func hasBindTarget(binds []string, containerPath string) bool { return false } +func hasBindSource(binds []string, hostPath string) bool { + for _, b := range binds { + parts := strings.Split(b, ":") + if len(parts) >= 2 && parts[0] == hostPath { + return true + } + } + return false +} + func cleanup() { ctx := context.Background() _ = dockerClient.ContainerStop(ctx, containerName, container.StopOptions{}) From 871aa13c0f6f5a56c03ff563f6e1cfde5140e0a4 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Sat, 21 Mar 2026 02:58:38 +0200 Subject: [PATCH 3/5] Fix SocketPath to return actual socket path for rootless Docker --- internal/runtime/docker.go | 34 ++++++++++++++++++++--- internal/runtime/docker_test.go | 49 +++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index 2a2c506..4856107 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -71,15 +71,41 @@ func probeSocket(candidates ...string) string { return "" } +// isVM reports whether the Docker daemon is running inside a VM (e.g., Colima, OrbStack). +// In these cases the socket is remapped inside the VM and the container sees it at +// /var/run/docker.sock even if the CLI connects via a user-scoped socket path. +func (d *DockerRuntime) isVM() bool { + host := d.client.DaemonHost() + if strings.HasPrefix(host, "unix://") { + socketPath := strings.TrimPrefix(host, "unix://") + // Check for known VM-based Docker socket locations + home, _ := os.UserHomeDir() + vmSockets := []string{ + filepath.Join(home, ".colima", "default", "docker.sock"), + filepath.Join(home, ".colima", "docker.sock"), + filepath.Join(home, ".orbstack", "run", "docker.sock"), + } + for _, vmSock := range vmSockets { + if socketPath == vmSock { + return true + } + } + } + return false +} + // SocketPath returns the daemon-visible Unix socket path to bind-mount into // containers so LocalStack can launch nested workloads such as Lambda functions. -// Even when the CLI connects through a user-scoped socket (for example Colima), -// the daemon resolves bind mounts inside its own environment where the socket is -// exposed at /var/run/docker.sock. +// For VM-based Docker (Colima, OrbStack) returns /var/run/docker.sock as the +// socket is remapped inside the VM. For rootless or custom setups, returns the +// actual socket path extracted from the daemon host. func (d *DockerRuntime) SocketPath() string { host := d.client.DaemonHost() if strings.HasPrefix(host, "unix://") { - return "/var/run/docker.sock" + if d.isVM() { + return "/var/run/docker.sock" + } + return strings.TrimPrefix(host, "unix://") } return "" } diff --git a/internal/runtime/docker_test.go b/internal/runtime/docker_test.go index 663a85d..7147b24 100644 --- a/internal/runtime/docker_test.go +++ b/internal/runtime/docker_test.go @@ -52,14 +52,14 @@ func TestSocketPath_ExtractsUnixPath(t *testing.T) { cli, err := client.NewClientWithOpts(client.WithHost("unix:///home/user/.colima/default/docker.sock")) require.NoError(t, err) rt := &DockerRuntime{client: cli} - assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + assert.Equal(t, "/home/user/.colima/default/docker.sock", rt.SocketPath()) }) t.Run("orbstack socket returns daemon path", func(t *testing.T) { cli, err := client.NewClientWithOpts(client.WithHost("unix:///Users/user/.orbstack/run/docker.sock")) require.NoError(t, err) rt := &DockerRuntime{client: cli} - assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + assert.Equal(t, "/Users/user/.orbstack/run/docker.sock", rt.SocketPath()) }) } @@ -71,6 +71,51 @@ func TestSocketPath_ReturnsEmptyForTCPHost(t *testing.T) { assert.Equal(t, "", rt.SocketPath()) } +func TestSocketPath_VMDetection(t *testing.T) { + home, err := os.UserHomeDir() + require.NoError(t, err) + + t.Run("colima socket exists returns remapped path", func(t *testing.T) { + colimaSock := filepath.Join(home, ".colima", "default", "docker.sock") + require.NoError(t, os.MkdirAll(filepath.Dir(colimaSock), 0o755)) + f, err := os.Create(colimaSock) + require.NoError(t, err) + f.Close() + defer os.Remove(colimaSock) + + cli, err := client.NewClientWithOpts(client.WithHost("unix://" + colimaSock)) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + }) + + t.Run("orbstack socket exists returns remapped path", func(t *testing.T) { + orbstackSock := filepath.Join(home, ".orbstack", "run", "docker.sock") + require.NoError(t, os.MkdirAll(filepath.Dir(orbstackSock), 0o755)) + f, err := os.Create(orbstackSock) + require.NoError(t, err) + f.Close() + defer os.Remove(orbstackSock) + + cli, err := client.NewClientWithOpts(client.WithHost("unix://" + orbstackSock)) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + }) + + t.Run("rootless socket exists returns actual path", func(t *testing.T) { + // Use a non-VM socket path (short path to avoid Docker client limit) + rootlessSock := "/tmp/lstk-docker.sock" + require.NoError(t, os.WriteFile(rootlessSock, nil, 0o600)) + defer os.Remove(rootlessSock) + + cli, err := client.NewClientWithOpts(client.WithHost("unix://" + rootlessSock)) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, rootlessSock, rt.SocketPath()) + }) +} + func TestWindowsDockerStartCommand_DockerAvailable(t *testing.T) { lookPath := func(string) (string, error) { return "/usr/bin/docker", nil } assert.Equal(t, "docker desktop start", windowsDockerStartCommand(func(string) string { return "" }, lookPath)) From 47b4db70c20dff30becab80f14eddfbb34d214b6 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Mon, 23 Mar 2026 11:12:36 +0200 Subject: [PATCH 4/5] Use isolated temp directory for VM detection tests --- internal/runtime/docker_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/runtime/docker_test.go b/internal/runtime/docker_test.go index 7147b24..7091d8f 100644 --- a/internal/runtime/docker_test.go +++ b/internal/runtime/docker_test.go @@ -72,8 +72,8 @@ func TestSocketPath_ReturnsEmptyForTCPHost(t *testing.T) { } func TestSocketPath_VMDetection(t *testing.T) { - home, err := os.UserHomeDir() - require.NoError(t, err) + home := t.TempDir() + t.Setenv("HOME", home) t.Run("colima socket exists returns remapped path", func(t *testing.T) { colimaSock := filepath.Join(home, ".colima", "default", "docker.sock") From e1b1a8810a5fd2a8f4bb969649844f4a869e96f2 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Mon, 23 Mar 2026 11:17:33 +0200 Subject: [PATCH 5/5] Fix errcheck lint errors --- internal/runtime/docker_test.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/runtime/docker_test.go b/internal/runtime/docker_test.go index 7091d8f..e9000eb 100644 --- a/internal/runtime/docker_test.go +++ b/internal/runtime/docker_test.go @@ -80,8 +80,10 @@ func TestSocketPath_VMDetection(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Dir(colimaSock), 0o755)) f, err := os.Create(colimaSock) require.NoError(t, err) - f.Close() - defer os.Remove(colimaSock) + require.NoError(t, f.Close()) + t.Cleanup(func() { + require.NoError(t, os.Remove(colimaSock)) + }) cli, err := client.NewClientWithOpts(client.WithHost("unix://" + colimaSock)) require.NoError(t, err) @@ -94,8 +96,10 @@ func TestSocketPath_VMDetection(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Dir(orbstackSock), 0o755)) f, err := os.Create(orbstackSock) require.NoError(t, err) - f.Close() - defer os.Remove(orbstackSock) + require.NoError(t, f.Close()) + t.Cleanup(func() { + require.NoError(t, os.Remove(orbstackSock)) + }) cli, err := client.NewClientWithOpts(client.WithHost("unix://" + orbstackSock)) require.NoError(t, err) @@ -107,7 +111,9 @@ func TestSocketPath_VMDetection(t *testing.T) { // Use a non-VM socket path (short path to avoid Docker client limit) rootlessSock := "/tmp/lstk-docker.sock" require.NoError(t, os.WriteFile(rootlessSock, nil, 0o600)) - defer os.Remove(rootlessSock) + t.Cleanup(func() { + require.NoError(t, os.Remove(rootlessSock)) + }) cli, err := client.NewClientWithOpts(client.WithHost("unix://" + rootlessSock)) require.NoError(t, err)