diff --git a/Testcontainers.sln b/Testcontainers.sln
index 7507b0faa..6494a2bb4 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -183,6 +183,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DockerCompose", "src\Testcontainers.DockerCompose\Testcontainers.DockerCompose.csproj", "{5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DockerCompose.Tests", "tests\Testcontainers.DockerCompose.Tests\Testcontainers.DockerCompose.Tests.csproj", "{D77017F1-9E38-4B06-8CEB-9B3D98B6497C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -532,6 +536,14 @@ Global
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D77017F1-9E38-4B06-8CEB-9B3D98B6497C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -619,5 +631,7 @@ Global
{1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+ {5CF21353-F3DB-4993-B9D8-DFAA8B2C4D2F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {D77017F1-9E38-4B06-8CEB-9B3D98B6497C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
diff --git a/src/Testcontainers.DockerCompose/.editorconfig b/src/Testcontainers.DockerCompose/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.DockerCompose/DockerComposeBuilder.cs b/src/Testcontainers.DockerCompose/DockerComposeBuilder.cs
new file mode 100644
index 000000000..121368d04
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/DockerComposeBuilder.cs
@@ -0,0 +1,123 @@
+namespace Testcontainers.DockerCompose;
+
+///
+[PublicAPI]
+public sealed class DockerComposeBuilder : ContainerBuilder
+{
+ private const string NoComposeFile = "No docker compose file have been provided.";
+
+ //Docker Compose is included as part of this image.
+ public const string DockerComposeImage = "docker:24-cli";
+
+ public const string DockerSocketPath = "/var/run/docker.sock";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DockerComposeBuilder()
+ : this(new DockerComposeConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private DockerComposeBuilder(DockerComposeConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override DockerComposeConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ public override DockerComposeContainer Build()
+ {
+ Validate();
+
+ return new DockerComposeContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
+ }
+
+ ///
+ /// Sets the compose file.
+ ///
+ /// The compose file.
+ ///
+ public DockerComposeBuilder WithComposeFile(string composeFile)
+ {
+ return Merge(DockerResourceConfiguration, new DockerComposeConfiguration
+ (composeFile: composeFile));
+ }
+
+ ///
+ /// If true use a local Docker Compose binary instead of a container.
+ ///
+ ///
+ ///
+ public DockerComposeBuilder WithLocalCompose(bool localCompose)
+ {
+ return Merge(DockerResourceConfiguration, new DockerComposeConfiguration
+ (localCompose: localCompose));
+ }
+
+ ///
+ protected override DockerComposeBuilder Init()
+ {
+ return base.Init()
+ .WithImage(DockerComposeImage)
+ .WithEntrypoint(CommonCommands.SleepInfinity)
+ .WithBindMount(DockerSocketPath, DockerSocketPath, AccessMode.ReadWrite)
+ .WithStartupCallback(ConfigureDockerComposeAsync);
+ }
+
+ ///
+ protected override void Validate()
+ {
+ base.Validate();
+
+ _ = Guard.Argument(DockerResourceConfiguration.ComposeFile, nameof(DockerResourceConfiguration.ComposeFile))
+ .NotEmpty();
+ }
+
+ ///
+ protected override DockerComposeBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new DockerComposeConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override DockerComposeBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new DockerComposeConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override DockerComposeBuilder Merge(DockerComposeConfiguration oldValue, DockerComposeConfiguration newValue)
+ {
+ return new DockerComposeBuilder(new DockerComposeConfiguration(oldValue, newValue));
+ }
+
+ ///
+ /// Configures the compose container.
+ ///
+ /// The container.
+ /// Cancellation token.
+ private async Task ConfigureDockerComposeAsync(IContainer container, CancellationToken ct = default)
+ {
+ if (container is DockerComposeRemote dockerComposeContainer &&
+ !dockerComposeContainer.RuntimeConfiguration.LocalCompose )
+ {
+ var fileInfo = new FileInfo(dockerComposeContainer.RuntimeConfiguration.ComposeFile);
+ if (fileInfo.Exists)
+ {
+ await container.CopyAsync(fileInfo, ".", Unix.FileMode644, ct)
+ .ConfigureAwait(false);
+ }
+ await container.ExecAsync(new[] { "docker", "compose", "up", "-d" }, ct)
+ .ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.DockerCompose/DockerComposeConfiguration.cs b/src/Testcontainers.DockerCompose/DockerComposeConfiguration.cs
new file mode 100644
index 000000000..1a163064c
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/DockerComposeConfiguration.cs
@@ -0,0 +1,71 @@
+namespace Testcontainers.DockerCompose;
+
+///
+[PublicAPI]
+public sealed class DockerComposeConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The fully qualified path to the compose file.
+ /// Whether the local compose will be used.
+ public DockerComposeConfiguration(
+ string composeFile = null,
+ bool localCompose = false)
+ {
+ ComposeFile = composeFile;
+ LocalCompose = localCompose;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public DockerComposeConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public DockerComposeConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public DockerComposeConfiguration(DockerComposeConfiguration resourceConfiguration)
+ : this(new DockerComposeConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public DockerComposeConfiguration(DockerComposeConfiguration oldValue, DockerComposeConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ ComposeFile = BuildConfiguration.Combine(oldValue.ComposeFile, newValue.ComposeFile);
+ LocalCompose = BuildConfiguration.Combine(oldValue.LocalCompose, newValue.LocalCompose);
+ }
+
+ ///
+ /// Gets the path to the compose file.
+ ///
+ public string ComposeFile { get; }
+
+ ///
+ /// Indicates whether local compose is enabled.
+ ///
+ public bool LocalCompose { get; }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.DockerCompose/DockerComposeContainer.cs b/src/Testcontainers.DockerCompose/DockerComposeContainer.cs
new file mode 100644
index 000000000..bb8688dfc
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/DockerComposeContainer.cs
@@ -0,0 +1,30 @@
+namespace Testcontainers.DockerCompose;
+
+public class DockerComposeContainer : DockerContainer
+{
+ private readonly IContainer _proxyContainer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ ///
+ public DockerComposeContainer(DockerComposeConfiguration configuration, ILogger logger) : base(configuration, logger)
+ {
+ _proxyContainer = configuration.LocalCompose
+ ? new DockerComposeLocal(configuration, logger)
+ : new DockerComposeRemote(configuration, logger);
+ }
+
+ ///
+ public override async Task StartAsync(CancellationToken ct = default)
+ {
+ await _proxyContainer.StartAsync(ct);
+ }
+
+ ///
+ public override async Task StopAsync(CancellationToken ct = default)
+ {
+ await _proxyContainer.StopAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.DockerCompose/DockerComposeLocal.cs b/src/Testcontainers.DockerCompose/DockerComposeLocal.cs
new file mode 100644
index 000000000..7ecf058f6
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/DockerComposeLocal.cs
@@ -0,0 +1,40 @@
+namespace Testcontainers.DockerCompose;
+
+[PublicAPI]
+public sealed class DockerComposeLocal : DockerContainer
+{
+ private readonly string _dockerComposeBinary =
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "docker-compose.exe" : "docker-compose";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ /// The logger.
+ public DockerComposeLocal(DockerComposeConfiguration configuration, ILogger logger) : base(configuration, logger)
+ {
+ }
+
+ ///
+ /// Gets the runtime configuration.
+ ///
+ public DockerComposeConfiguration RuntimeConfiguration => _configuration as DockerComposeConfiguration;
+
+ ///
+ public override async Task StartAsync(CancellationToken ct = default)
+ {
+ await Cli.Wrap(_dockerComposeBinary)
+ .WithArguments(new[] {"up", "-d"})
+ .WithWorkingDirectory(Path.GetDirectoryName(RuntimeConfiguration.ComposeFile)!)
+ .ExecuteBufferedAsync();
+ }
+
+ ///
+ public override async Task StopAsync(CancellationToken ct = default)
+ {
+ await Cli.Wrap(_dockerComposeBinary)
+ .WithArguments(new[] {"down"})
+ .WithWorkingDirectory(Path.GetDirectoryName(RuntimeConfiguration.ComposeFile)!)
+ .ExecuteBufferedAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.DockerCompose/DockerComposeRemote.cs b/src/Testcontainers.DockerCompose/DockerComposeRemote.cs
new file mode 100644
index 000000000..d2ca4e1b8
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/DockerComposeRemote.cs
@@ -0,0 +1,29 @@
+namespace Testcontainers.DockerCompose;
+
+///
+[PublicAPI]
+public class DockerComposeRemote : DockerContainer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ /// The logger.
+ public DockerComposeRemote(DockerComposeConfiguration configuration, ILogger logger)
+ : base(configuration, logger)
+ {
+ }
+
+ ///
+ /// Gets the runtime configuration.
+ ///
+ public DockerComposeConfiguration RuntimeConfiguration => _configuration as DockerComposeConfiguration;
+
+ ///
+ public override async Task StopAsync(CancellationToken ct = default)
+ {
+ await ExecAsync(new[] { "docker", "compose", "down"}, ct)
+ .ConfigureAwait(false);
+ await base.StopAsync(ct);
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.DockerCompose/Testcontainers.DockerCompose.csproj b/src/Testcontainers.DockerCompose/Testcontainers.DockerCompose.csproj
new file mode 100644
index 000000000..894c19cd2
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/Testcontainers.DockerCompose.csproj
@@ -0,0 +1,15 @@
+
+
+ netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.DockerCompose/Usings.cs b/src/Testcontainers.DockerCompose/Usings.cs
new file mode 100644
index 000000000..fbfb35ec4
--- /dev/null
+++ b/src/Testcontainers.DockerCompose/Usings.cs
@@ -0,0 +1,18 @@
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using JetBrains.Annotations;
+global using Microsoft.Extensions.Logging;
+global using System.Diagnostics;
+global using System.Linq;
+global using System.Runtime.InteropServices;
+global using CliWrap;
+global using CliWrap.Buffered;
+global using DotNet.Testcontainers.Commons;
\ No newline at end of file
diff --git a/src/Testcontainers/Containers/DockerContainer.cs b/src/Testcontainers/Containers/DockerContainer.cs
index 1e9bbf855..a84e9c64b 100644
--- a/src/Testcontainers/Containers/DockerContainer.cs
+++ b/src/Testcontainers/Containers/DockerContainer.cs
@@ -24,9 +24,9 @@ public class DockerContainer : Resource, IContainer
private readonly ITestcontainersClient _client;
- private readonly IContainerConfiguration _configuration;
+ protected ContainerInspectResponse _container = new ContainerInspectResponse();
- private ContainerInspectResponse _container = new ContainerInspectResponse();
+ protected readonly IContainerConfiguration _configuration;
///
/// Initializes a new instance of the class.
diff --git a/tests/Testcontainers.DockerCompose.Tests/.editorconfig b/tests/Testcontainers.DockerCompose.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.DockerCompose.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.DockerCompose.Tests/DockerComposeRemoteTest.cs b/tests/Testcontainers.DockerCompose.Tests/DockerComposeRemoteTest.cs
new file mode 100644
index 000000000..adfbee1b3
--- /dev/null
+++ b/tests/Testcontainers.DockerCompose.Tests/DockerComposeRemoteTest.cs
@@ -0,0 +1,55 @@
+using JetBrains.Annotations;
+
+namespace Testcontainers.DockerCompose;
+
+public abstract class DockerComposeRemoteTest : IAsyncLifetime
+{
+ private readonly DockerComposeContainer _dockerComposeContainer;
+
+ protected DockerComposeRemoteTest(DockerComposeContainer dockerComposeContainer)
+ {
+ _dockerComposeContainer = dockerComposeContainer;
+ }
+
+ public Task InitializeAsync()
+ {
+ return _dockerComposeContainer.StartAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _dockerComposeContainer.StopAsync();
+ await _dockerComposeContainer.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public void ContainerStartedSuccessfully()
+ {
+ Assert.Equal(TestcontainersHealthStatus.Healthy, TestcontainersHealthStatus.Healthy);
+ }
+
+ [UsedImplicitly]
+ public sealed class DockerComposeRemoteConfiguration : DockerComposeRemoteTest
+ {
+ public DockerComposeRemoteConfiguration()
+ : base(new DockerComposeBuilder()
+ .WithComposeFile(Path.Combine(Directory.GetCurrentDirectory(), @"./../../../docker-compose.yaml"))
+ .Build())
+ {
+ }
+ }
+
+ [UsedImplicitly]
+ public sealed class DockerComposeLocalConfiguration : DockerComposeRemoteTest
+ {
+ public DockerComposeLocalConfiguration()
+ : base(new DockerComposeBuilder()
+ .WithComposeFile(Path.Combine(Directory.GetCurrentDirectory(), @"./../../../docker-compose.yaml"))
+ .WithLocalCompose(true)
+ .Build())
+ {
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/tests/Testcontainers.DockerCompose.Tests/Testcontainers.DockerCompose.Tests.csproj b/tests/Testcontainers.DockerCompose.Tests/Testcontainers.DockerCompose.Tests.csproj
new file mode 100644
index 000000000..3afc73302
--- /dev/null
+++ b/tests/Testcontainers.DockerCompose.Tests/Testcontainers.DockerCompose.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+ net8.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Testcontainers.DockerCompose.Tests/Usings.cs b/tests/Testcontainers.DockerCompose.Tests/Usings.cs
new file mode 100644
index 000000000..784a06819
--- /dev/null
+++ b/tests/Testcontainers.DockerCompose.Tests/Usings.cs
@@ -0,0 +1,6 @@
+global using System.IO;
+global using System.Threading.Tasks;
+global using DotNet.Testcontainers.Commons;
+global using DotNet.Testcontainers.Containers;
+global using Testcontainers.DockerCompose;
+global using Xunit;
\ No newline at end of file
diff --git a/tests/Testcontainers.DockerCompose.Tests/docker-compose.yaml b/tests/Testcontainers.DockerCompose.Tests/docker-compose.yaml
new file mode 100644
index 000000000..d7effad58
--- /dev/null
+++ b/tests/Testcontainers.DockerCompose.Tests/docker-compose.yaml
@@ -0,0 +1,9 @@
+version: '3.9'
+
+name: test-nginx
+
+services:
+ nginx:
+ image: nginx
+ ports:
+ - "8080:80"