DEV Community

Saulo Dias
Saulo Dias

Posted on

Avoiding Port Conflicts in Testcontainers with Random Port Binding

When writing integration tests with Testcontainers for .NET, one of the most common problems is port conflicts.

Imagine you have multiple tests that spin up containers in parallel. If they all try to bind the same host port (e.g., 6379 for Redis or 80 for Nginx), you’ll quickly run into errors like:

Bind for 0.0.0.0:6379 failed: port is already allocated
Enter fullscreen mode Exit fullscreen mode

The fix is surprisingly simple: let Docker assign a random host port instead of forcing a fixed one.


The Trick

When you expose a container port, you can tell Testcontainers to map it to a free random port on the host:

.WithPortBinding(internalPort, assignRandomHostPort: true)
Enter fullscreen mode Exit fullscreen mode

This avoids conflicts and allows your tests to run in parallel without stepping on each other.


Minimal Example

Here’s a lightweight example using nginx:alpine:

using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;

public class WebServerTestContainer : IAsyncDisposable
{
    private readonly IContainer _container;
    private readonly TaskCompletionSource<int> _portInitialized = new();
    private readonly int _internalPort = 80;

    public int Port => _portInitialized.Task.Result;

    public WebServerTestContainer()
    {
        _container = new ContainerBuilder()
            .WithImage("nginx:alpine")
            .WithExposedPort(_internalPort)
            .WithPortBinding(_internalPort, true) // <— random host port
            .WithWaitStrategy(
                Wait.ForUnixContainer()
                    .UntilHttpRequestIsSucceeded(r => r
                        .ForPort(_internalPort)
                        .ForPath("/")))
            .Build();
    }

    public async Task StartAsync()
    {
        await _container.StartAsync();
        _portInitialized.SetResult(_container.GetMappedPublicPort(_internalPort));
    }

    public async ValueTask DisposeAsync() => await _container.DisposeAsync();
}
Enter fullscreen mode Exit fullscreen mode

Using It in a Test

public class WebServerTests : IAsyncLifetime
{
    private readonly WebServerTestContainer _server = new();

    public async Task InitializeAsync()
    {
        await _server.StartAsync();
    }

    public async Task DisposeAsync()
    {
        await _server.DisposeAsync();
    }

    [Fact]
    public async Task ShouldRespond()
    {
        using var http = new HttpClient();
        var response = await http.GetAsync($"http://localhost:{_server.Port}/");
        Assert.True(response.IsSuccessStatusCode);
    }
}
Enter fullscreen mode Exit fullscreen mode

Why This Matters

  • ✅ No more port conflicts
  • ✅ Tests can safely run in parallel
  • ✅ No need to hardcode host ports

This small trick makes your integration tests more reliable and parallelizable without changing much code.

Top comments (0)