diff --git a/README.md b/README.md
index 1c42db1..0b3185e 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,9 @@ builder.AddLocalStack(configureContainer: container =>
// Optional: Enable verbose logging for troubleshooting
container.DebugLevel = 1;
container.LogLevel = LocalStackLogLevel.Debug;
+
+ // Optional: Use a specific port instead of dynamic port assignment
+ container.Port = 4566;
});
```
@@ -103,6 +106,7 @@ builder.AddLocalStack(configureContainer: container =>
- **`Lifetime`** - Container lifecycle: `Persistent` (survives restarts) or `Session` (cleaned up on stop)
- **`DebugLevel`** - LocalStack debug verbosity (0 = default, 1 = verbose)
- **`LogLevel`** - Log level control (Error, Warn, Info, Debug, Trace, etc.)
+- **`Port`** - Static port mapping for LocalStack container. If set, LocalStack will be mapped to this static port on the host. If not set, a dynamic port will be used unless the container lifetime is persistent, in which case the default LocalStack port (4566) is used. Useful for avoiding port conflicts or for predictable endpoint URLs
- **`AdditionalEnvironmentVariables`** - Custom environment variables for advanced scenarios
For detailed configuration guide and best practices, see [Configuration Documentation](docs/CONFIGURATION.md).
diff --git a/src/Aspire.Hosting.LocalStack/Container/LocalStackContainerOptions.cs b/src/Aspire.Hosting.LocalStack/Container/LocalStackContainerOptions.cs
index 1b6dcbb..882d87b 100644
--- a/src/Aspire.Hosting.LocalStack/Container/LocalStackContainerOptions.cs
+++ b/src/Aspire.Hosting.LocalStack/Container/LocalStackContainerOptions.cs
@@ -62,4 +62,13 @@ public sealed class LocalStackContainerOptions
/// Default: false
///
public bool EnableDockerSocket { get; set; }
+
+ ///
+ /// Gets or sets the port to expose LocalStack on the host machine.
+ ///
+ ///
+ /// If set, LocalStack will be mapped to this static port on the host. If not set, a dynamic port will be used unless the container lifetime is persistent,
+ /// in which case the default LocalStack port is used. Useful for avoiding port conflicts or for predictable endpoint URLs.
+ ///
+ public int? Port { get; set; }
}
diff --git a/src/Aspire.Hosting.LocalStack/LocalStackResourceBuilderExtensions.cs b/src/Aspire.Hosting.LocalStack/LocalStackResourceBuilderExtensions.cs
index 79c79ef..a4654c3 100644
--- a/src/Aspire.Hosting.LocalStack/LocalStackResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.LocalStack/LocalStackResourceBuilderExtensions.cs
@@ -171,7 +171,10 @@ public static IDistributedApplicationBuilder UseLocalStack(this IDistributedAppl
.WithImage(LocalStackContainerImageTags.Image)
.WithImageRegistry(LocalStackContainerImageTags.Registry)
.WithImageTag(LocalStackContainerImageTags.Tag)
- .WithHttpEndpoint(targetPort: Constants.DefaultContainerPort, name: LocalStackResource.PrimaryEndpointName)
+ .WithHttpEndpoint(targetPort: Constants.DefaultContainerPort,
+ // Map to a static port if specified or if using a persistent lifetime; otherwise, use dynamic port mapping
+ port: containerOptions.Port ?? (containerOptions.Lifetime == ContainerLifetime.Persistent ? Constants.DefaultContainerPort : null),
+ name: LocalStackResource.PrimaryEndpointName)
.WithLifetime(containerOptions.Lifetime)
.WithEnvironment("DEBUG", containerOptions.DebugLevel.ToString(CultureInfo.InvariantCulture))
.WithEnvironment("LS_LOG", containerOptions.LogLevel.ToEnvironmentValue())
diff --git a/tests/Aspire.Hosting.LocalStack.Unit.Tests/Container/LocalStackContainerOptionsTests.cs b/tests/Aspire.Hosting.LocalStack.Unit.Tests/Container/LocalStackContainerOptionsTests.cs
index fc0b63d..f4a318f 100644
--- a/tests/Aspire.Hosting.LocalStack.Unit.Tests/Container/LocalStackContainerOptionsTests.cs
+++ b/tests/Aspire.Hosting.LocalStack.Unit.Tests/Container/LocalStackContainerOptionsTests.cs
@@ -98,4 +98,23 @@ public void EnableDockerSocket_Should_Be_Settable()
Assert.True(options.EnableDockerSocket);
}
+
+ [Fact]
+ public void Port_Should_Default_To_Null()
+ {
+ var options = new LocalStackContainerOptions();
+
+ Assert.Null(options.Port);
+ }
+
+ [Fact]
+ public void Port_Should_Be_Settable()
+ {
+ var options = new LocalStackContainerOptions
+ {
+ Port = 1234,
+ };
+
+ Assert.Equal(1234, options.Port);
+ }
}
diff --git a/tests/Aspire.Hosting.LocalStack.Unit.Tests/Extensions/ResourceBuilderExtensionsTests/AddLocalStackTests.cs b/tests/Aspire.Hosting.LocalStack.Unit.Tests/Extensions/ResourceBuilderExtensionsTests/AddLocalStackTests.cs
index 29b916a..1cd100c 100644
--- a/tests/Aspire.Hosting.LocalStack.Unit.Tests/Extensions/ResourceBuilderExtensionsTests/AddLocalStackTests.cs
+++ b/tests/Aspire.Hosting.LocalStack.Unit.Tests/Extensions/ResourceBuilderExtensionsTests/AddLocalStackTests.cs
@@ -288,4 +288,35 @@ public void AddLocalStack_Should_Not_Mount_Docker_Socket_By_Default()
Assert.Null(dockerSocketMount);
}
+
+ [Theory]
+ [InlineData(ContainerLifetime.Session, null, null)]
+ [InlineData(ContainerLifetime.Session, 1234, 1234)]
+ [InlineData(ContainerLifetime.Persistent, null, Constants.DefaultContainerPort)]
+ [InlineData(ContainerLifetime.Persistent, 1234, 1234)]
+ public void AddLocalStack_Should_Set_Endpoint_Port(ContainerLifetime lifetime, int? port, int? expectedPort)
+ {
+ var builder = DistributedApplication.CreateBuilder([]);
+ var (localStackOptions, _, _) = TestDataBuilders.CreateMockLocalStackOptions(useLocalStack: true);
+
+ var result = builder.AddLocalStack
+ (
+ localStackOptions: localStackOptions,
+ configureContainer: container =>
+ {
+ container.Lifetime = lifetime;
+ container.Port = port;
+ });
+
+ Assert.NotNull(result);
+ var resource = result.Resource;
+ Assert.NotNull(resource);
+
+ // Verify endpoint port configuration
+ var endpointAnnotations = resource.Annotations.OfType();
+ var httpEndpoint = endpointAnnotations.FirstOrDefault(e => e is { Name: "http" });
+
+ Assert.NotNull(httpEndpoint);
+ Assert.Equal(expectedPort, httpEndpoint.Port);
+ }
}