From 78d2e6de66e3c464f0963ab0f5a8a7abdcacf229 Mon Sep 17 00:00:00 2001 From: Anisa Oshafi Date: Thu, 12 Mar 2026 18:25:11 +0100 Subject: [PATCH 1/4] fix: pass GATEWAY_LISTEN env var for non-default ports --- internal/container/start.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/container/start.go b/internal/container/start.go index 9b489f5..baa06b5 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -7,6 +7,7 @@ import ( "os" stdruntime "runtime" "slices" + "strings" "time" "github.com/containerd/errdefs" @@ -74,6 +75,9 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start return err } env := append(resolvedEnv, "LOCALSTACK_AUTH_TOKEN="+token) + if needsGatewayListen(c.Port, resolvedEnv) { + env = append(env, "GATEWAY_LISTEN=:"+c.Port) + } containers[i] = runtime.ContainerConfig{ Image: image, Name: c.Name(), @@ -318,3 +322,11 @@ func hasDuplicateContainerTypes(containers []config.ContainerConfig) bool { } return false } + +func needsGatewayListen(port string, env []string) bool { + // LocalStack only binds to the configured port when GATEWAY_LISTEN is set explicitly; + // without it, non-default ports cause the emulator to never become ready. + return port != "4566" && !slices.ContainsFunc(env, func(e string) bool { + return strings.HasPrefix(e, "GATEWAY_LISTEN=") + }) +} From ce6eef2c02807faefa518c0ce06799012b303641 Mon Sep 17 00:00:00 2001 From: Anisa Oshafi Date: Thu, 12 Mar 2026 18:58:38 +0100 Subject: [PATCH 2/4] add test --- test/integration/start_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 2bf88f6..8f79a72 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -3,6 +3,9 @@ package integration_test import ( "context" "net" + "net/http" + "os" + "path/filepath" "testing" "github.com/docker/docker/api/types/container" @@ -95,6 +98,37 @@ func TestStartCommandFailsWhenPortInUse(t *testing.T) { assert.Contains(t, stdout, "lstk stop") } +func TestStartCommandSucceedsWithNonDefaultPort(t *testing.T) { + requireDocker(t) + _ = env.Require(t, env.AuthToken) + + cleanup() + t.Cleanup(cleanup) + + mockServer := createMockLicenseServer(true) + defer mockServer.Close() + + configContent := ` +[[containers]] +type = "aws" +tag = "latest" +port = "4567" +` + configFile := filepath.Join(t.TempDir(), "config.toml") + require.NoError(t, os.WriteFile(configFile, []byte(configContent), 0644)) + + ctx := testContext(t) + _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "--config", configFile, "start") + require.NoError(t, err, "lstk start failed: %s", stderr) + + // lstk start only returns once the health check passes, but verify directly + // that LocalStack is reachable on the non-default port. + resp, err := http.Get("http://localhost:4567/_localstack/health") + require.NoError(t, err, "LocalStack health endpoint not reachable on port 4567") + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode, "LocalStack should be ready on port 4567") +} + func cleanup() { ctx := context.Background() _ = dockerClient.ContainerStop(ctx, containerName, container.StopOptions{}) From 62517d32da475d4723257b23de78cb011022107f Mon Sep 17 00:00:00 2001 From: Anisa Oshafi Date: Thu, 12 Mar 2026 19:26:35 +0100 Subject: [PATCH 3/4] Refactor --- internal/container/start.go | 12 ------------ internal/runtime/docker.go | 6 +++--- test/integration/start_test.go | 8 -------- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/internal/container/start.go b/internal/container/start.go index baa06b5..9b489f5 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -7,7 +7,6 @@ import ( "os" stdruntime "runtime" "slices" - "strings" "time" "github.com/containerd/errdefs" @@ -75,9 +74,6 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start return err } env := append(resolvedEnv, "LOCALSTACK_AUTH_TOKEN="+token) - if needsGatewayListen(c.Port, resolvedEnv) { - env = append(env, "GATEWAY_LISTEN=:"+c.Port) - } containers[i] = runtime.ContainerConfig{ Image: image, Name: c.Name(), @@ -322,11 +318,3 @@ func hasDuplicateContainerTypes(containers []config.ContainerConfig) bool { } return false } - -func needsGatewayListen(port string, env []string) bool { - // LocalStack only binds to the configured port when GATEWAY_LISTEN is set explicitly; - // without it, non-default ports cause the emulator to never become ready. - return port != "4566" && !slices.ContainsFunc(env, func(e string) bool { - return strings.HasPrefix(e, "GATEWAY_LISTEN=") - }) -} diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index dddb9af..e2d5149 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -108,9 +108,9 @@ func (d *DockerRuntime) PullImage(ctx context.Context, imageName string, progres } func (d *DockerRuntime) Start(ctx context.Context, config ContainerConfig) (string, error) { - port := nat.Port(config.Port + "/tcp") - exposedPorts := nat.PortSet{port: struct{}{}} - portBindings := nat.PortMap{port: []nat.PortBinding{{HostPort: config.Port}}} + containerPort := nat.Port("4566/tcp") + exposedPorts := nat.PortSet{containerPort: struct{}{}} + portBindings := nat.PortMap{containerPort: []nat.PortBinding{{HostPort: config.Port}}} resp, err := d.client.ContainerCreate(ctx, &container.Config{ diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 8f79a72..ec8246a 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -3,7 +3,6 @@ package integration_test import ( "context" "net" - "net/http" "os" "path/filepath" "testing" @@ -120,13 +119,6 @@ port = "4567" ctx := testContext(t) _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "--config", configFile, "start") require.NoError(t, err, "lstk start failed: %s", stderr) - - // lstk start only returns once the health check passes, but verify directly - // that LocalStack is reachable on the non-default port. - resp, err := http.Get("http://localhost:4567/_localstack/health") - require.NoError(t, err, "LocalStack health endpoint not reachable on port 4567") - defer resp.Body.Close() - assert.Equal(t, http.StatusOK, resp.StatusCode, "LocalStack should be ready on port 4567") } func cleanup() { From 4a6b0e9699d15a8949a2744b5ecf9f998e581dbf Mon Sep 17 00:00:00 2001 From: Anisa Oshafi Date: Thu, 12 Mar 2026 19:31:10 +0100 Subject: [PATCH 4/4] Nit --- internal/runtime/docker.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index e2d5149..f00c0ca 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -107,8 +107,10 @@ func (d *DockerRuntime) PullImage(ctx context.Context, imageName string, progres return nil } +const emulatorContainerPort = nat.Port("4566/tcp") + func (d *DockerRuntime) Start(ctx context.Context, config ContainerConfig) (string, error) { - containerPort := nat.Port("4566/tcp") + containerPort := emulatorContainerPort exposedPorts := nat.PortSet{containerPort: struct{}{}} portBindings := nat.PortMap{containerPort: []nat.PortBinding{{HostPort: config.Port}}}