Launch and manage multi-container Docker Compose environments for integration testing. Testcontainers provides support for running complete Docker Compose applications, enabling tests against complex multi-service architectures defined in compose files.
Launch Docker Compose environments from compose files with full control over service exposure and lifecycle.
/**
* Container that manages a Docker Compose environment.
* Starts all services defined in compose file(s) and provides access to them.
*
* @param <SELF> Self-referential generic type for fluent API
*/
public class DockerComposeContainer<SELF extends DockerComposeContainer<SELF>>
implements Startable {
/**
* Create a Docker Compose container from a single compose file.
*
* @param composeFile the compose file
* @deprecated use constructor with DockerImageName as first parameter
*/
@Deprecated
public DockerComposeContainer(File composeFile);
/**
* Create a Docker Compose container from multiple compose files.
* Files are merged in order (later files override earlier ones).
*
* @param composeFiles list of compose files
* @deprecated use constructor with DockerImageName as first parameter
*/
@Deprecated
public DockerComposeContainer(List<File> composeFiles);
/**
* Create a Docker Compose container with a custom docker compose image.
*
* @param image custom docker compose image
* @param composeFiles compose files
*/
public DockerComposeContainer(DockerImageName image, File... composeFiles);
/**
* Create a Docker Compose container with a custom docker compose image.
*
* @param image custom docker compose image
* @param composeFiles list of compose files
*/
public DockerComposeContainer(DockerImageName image, List<File> composeFiles);
/**
* Create a Docker Compose container with custom image and identifier.
*
* @param image custom docker compose image
* @param identifier unique identifier for the environment
* @param composeFiles compose files
*/
public DockerComposeContainer(DockerImageName image, String identifier, File... composeFiles);
/**
* Create a Docker Compose container with custom image, identifier, and single file.
*
* @param image custom docker compose image
* @param identifier unique identifier for the environment
* @param composeFile compose file
*/
public DockerComposeContainer(DockerImageName image, String identifier, File composeFile);
/**
* Create a Docker Compose container with custom image, identifier, and list of files.
*
* @param image custom docker compose image
* @param identifier unique identifier for the environment
* @param composeFiles list of compose files
*/
public DockerComposeContainer(DockerImageName image, String identifier, List<File> composeFiles);
/**
* Create a Docker Compose container with identifier.
*
* @param identifier unique identifier for the environment
* @param composeFiles compose files
* @deprecated use constructor with DockerImageName as first parameter
*/
@Deprecated
public DockerComposeContainer(String identifier, File... composeFiles);
/**
* Create a Docker Compose container with identifier.
*
* @param identifier unique identifier for the environment
* @param composeFiles list of compose files
* @deprecated use constructor with DockerImageName as first parameter
*/
@Deprecated
public DockerComposeContainer(String identifier, List<File> composeFiles);
/**
* Start only specific services (instead of all services in the compose file).
*
* @param services names of services to start
* @return this container for method chaining
*/
public SELF withServices(@NonNull String... services);
/**
* Expose a service port to the host.
*
* @param serviceName name of the service in docker-compose.yml
* @param servicePort port number in the service
* @return this container for method chaining
*/
public SELF withExposedService(String serviceName, int servicePort);
/**
* Expose a service port with a wait strategy.
*
* @param serviceName name of the service
* @param servicePort port number
* @param waitStrategy wait strategy for the service
* @return this container for method chaining
*/
public SELF withExposedService(String serviceName, int servicePort, WaitStrategy waitStrategy);
/**
* Expose a specific instance of a scaled service.
* Note: Returns DockerComposeContainer, not SELF generic type.
*
* @param serviceName name of the service
* @param instance instance number (1-based)
* @param servicePort port number
* @return this container for method chaining
*/
public DockerComposeContainer withExposedService(String serviceName, int instance, int servicePort);
/**
* Expose a specific instance of a scaled service with a wait strategy.
* Note: Returns DockerComposeContainer, not SELF generic type.
*
* @param serviceName name of the service
* @param instance instance number (1-based)
* @param servicePort port number
* @param waitStrategy wait strategy for the service
* @return this container for method chaining
*/
public DockerComposeContainer withExposedService(String serviceName, int instance, int servicePort, WaitStrategy waitStrategy);
/**
* Set a wait strategy for a service without exposing ports.
*
* @param serviceName name of the service
* @param waitStrategy wait strategy for the service
* @return this container for method chaining
*/
public SELF waitingFor(String serviceName, WaitStrategy waitStrategy);
/**
* Scale a service to multiple instances.
*
* @param serviceName name of the service to scale
* @param numInstances number of instances to run
* @return this container for method chaining
*/
public SELF withScaledService(String serviceName, int numInstances);
/**
* Control whether to pull images before starting.
*
* @param pull whether to pull images
* @return this container for method chaining
*/
public SELF withPull(boolean pull);
/**
* Control whether to build images before starting.
*
* @param build whether to build images
* @return this container for method chaining
*/
public SELF withBuild(boolean build);
/**
* Set an environment variable for all services.
*
* @param key environment variable name
* @param value environment variable value
* @return this container for method chaining
*/
public SELF withEnv(String key, String value);
/**
* Set multiple environment variables for all services.
*
* @param env map of environment variables
* @return this container for method chaining
*/
public SELF withEnv(Map<String, String> env);
/**
* Set docker-compose command options.
*
* @param options command-line options
* @return this container for method chaining
*/
public SELF withOptions(String... options);
/**
* Configure image removal behavior after shutdown.
*
* @param removeImages which images to remove
* @return this container for method chaining
*/
public SELF withRemoveImages(RemoveImages removeImages);
/**
* Configure volume removal behavior after shutdown.
*
* @param removeVolumes whether to remove volumes
* @return this container for method chaining
*/
public SELF withRemoveVolumes(boolean removeVolumes);
/**
* Enable tailing logs from child containers.
*
* @param tailChildContainers whether to tail logs
* @return this container for method chaining
*/
public SELF withTailChildContainers(boolean tailChildContainers);
/**
* Set a log consumer for a specific service.
*
* @param serviceName name of the service
* @param consumer consumer to receive log output
* @return this container for method chaining
*/
public SELF withLogConsumer(String serviceName, Consumer<OutputFrame> consumer);
/**
* Set the startup timeout for all services.
*
* @param startupTimeout the startup timeout duration
* @return this container for method chaining
*/
public SELF withStartupTimeout(Duration startupTimeout);
/**
* Copy files into containers based on include patterns.
*
* @param fileCopyInclusions patterns for files to copy
* @return this container for method chaining
*/
public SELF withCopyFilesInContainer(String... fileCopyInclusions);
/**
* Get the host for accessing a service.
*
* @param serviceName name of the service
* @param servicePort port of the service
* @return host address
*/
public String getServiceHost(String serviceName, Integer servicePort);
/**
* Get the mapped port for accessing a service from the host.
*
* @param serviceName name of the service
* @param servicePort original port in the service
* @return mapped port on the host
*/
public Integer getServicePort(String serviceName, Integer servicePort);
/**
* Get the container state for a specific service.
*
* @param serviceName name of the service
* @return optional container state, empty if service not found
*/
public Optional<ContainerState> getContainerByServiceName(String serviceName);
/**
* Start all services in the compose environment.
*/
@Override
public void start();
/**
* Stop all services in the compose environment.
*/
@Override
public void stop();
/**
* Image removal options for compose down operation.
* Note: Each class (DockerComposeContainer and ComposeContainer) defines its own enum.
*/
public enum RemoveImages {
/** Remove all images used by any service */
ALL,
/** Remove only images that don't have a custom tag set by the `image` field */
LOCAL
}
}Usage Examples:
import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.File;
import java.time.Duration;
// Basic Docker Compose setup
DockerComposeContainer<?> environment = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withExposedService("web", 8080)
.withExposedService("db", 5432);
environment.start();
// Get connection details
String webHost = environment.getServiceHost("web", 8080);
Integer webPort = environment.getServicePort("web", 8080);
String webUrl = String.format("http://%s:%d", webHost, webPort);
// With wait strategies
DockerComposeContainer<?> withWaits = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withExposedService("api", 8080,
Wait.forHttp("/health")
.forStatusCode(200)
.withStartupTimeout(Duration.ofSeconds(60)))
.withExposedService("database", 5432,
Wait.forListeningPort()
.withStartupTimeout(Duration.ofSeconds(30)));
withWaits.start();
// Multiple compose files (override pattern)
DockerComposeContainer<?> multiFile = new DockerComposeContainer<>(
List.of(
new File("docker-compose.yml"),
new File("docker-compose.test.yml") // Overrides for testing
))
.withExposedService("app", 8080);
// Service scaling
DockerComposeContainer<?> scaled = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withExposedService("worker", 8080)
.withScaledService("worker", 3); // Run 3 worker instances
scaled.start();
// Environment variables
DockerComposeContainer<?> withEnv = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withEnv("ENVIRONMENT", "test")
.withEnv("LOG_LEVEL", "DEBUG")
.withExposedService("app", 8080);
// Using try-with-resources
try (DockerComposeContainer<?> env = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withExposedService("web", 80)) {
env.start();
// Run tests
String url = String.format("http://%s:%d",
env.getServiceHost("web", 80),
env.getServicePort("web", 80));
} // Environment automatically stopped and cleaned upModern Docker Compose V2 support (alternative to DockerComposeContainer).
/**
* Modern Docker Compose V2 container implementation.
* Uses either Compose V2 in Docker binary or containerized Compose V2.
*/
public class ComposeContainer implements Startable {
// Constructors
public ComposeContainer(DockerImageName image, File... composeFiles);
public ComposeContainer(DockerImageName image, List<File> composeFiles);
public ComposeContainer(DockerImageName image, String identifier, File... composeFiles);
public ComposeContainer(DockerImageName image, String identifier, File composeFile);
public ComposeContainer(DockerImageName image, String identifier, List<File> composeFiles);
public ComposeContainer(File... composeFiles);
public ComposeContainer(List<File> composeFiles);
public ComposeContainer(String identifier, File... composeFiles);
public ComposeContainer(String identifier, List<File> composeFiles);
// Service configuration
public ComposeContainer withServices(String... services);
public ComposeContainer withExposedService(String serviceName, int servicePort);
public ComposeContainer withExposedService(String serviceName, int instance, int servicePort);
public ComposeContainer withExposedService(String serviceName, int servicePort, WaitStrategy waitStrategy);
public ComposeContainer withExposedService(String serviceName, int instance, int servicePort, WaitStrategy waitStrategy);
public ComposeContainer waitingFor(String serviceName, WaitStrategy waitStrategy);
public ComposeContainer withScaledService(String serviceBaseName, int numInstances);
// Environment and configuration
public ComposeContainer withEnv(String key, String value);
public ComposeContainer withEnv(Map<String, String> env);
public ComposeContainer withOptions(String... options);
public ComposeContainer withBuild(boolean build);
public ComposeContainer withPull(boolean pull);
public ComposeContainer withRemoveImages(RemoveImages removeImages);
public ComposeContainer withRemoveVolumes(boolean removeVolumes);
public ComposeContainer withStartupTimeout(Duration startupTimeout);
public ComposeContainer withTailChildContainers(boolean tailChildContainers);
public ComposeContainer withLogConsumer(String serviceName, Consumer<OutputFrame> consumer);
public ComposeContainer withCopyFilesInContainer(String... fileCopyInclusions);
// Service access
public String getServiceHost(String serviceName, Integer servicePort);
public Integer getServicePort(String serviceName, Integer servicePort);
public Optional<ContainerState> getContainerByServiceName(String serviceName);
// Lifecycle
@Override
public void start();
@Override
public void stop();
/**
* Image removal options for compose down.
* Note: Each class (DockerComposeContainer and ComposeContainer) defines its own enum.
*/
public enum RemoveImages {
/** Remove all images used by any service */
ALL,
/** Remove only images that don't have a custom tag set by the `image` field */
LOCAL
}
}import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.time.Duration;
public class FullStackIntegrationTest {
@Test
public void testFullApplication() {
// docker-compose.yml contains:
// - postgres (database)
// - redis (cache)
// - api (backend service)
// - web (frontend)
try (DockerComposeContainer<?> environment =
new DockerComposeContainer<>(new File("docker-compose.yml"))
// Database
.withExposedService("postgres", 5432,
Wait.forListeningPort()
.withStartupTimeout(Duration.ofSeconds(60)))
// Cache
.withExposedService("redis", 6379,
Wait.forListeningPort())
// API with health check
.withExposedService("api", 8080,
Wait.forHttp("/health")
.forStatusCode(200)
.withStartupTimeout(Duration.ofMinutes(2)))
// Frontend
.withExposedService("web", 80,
Wait.forHttp("/")
.forStatusCode(200))
// Environment configuration
.withEnv("ENVIRONMENT", "test")
.withEnv("LOG_LEVEL", "INFO")) {
environment.start();
// Get service URLs
String apiUrl = String.format("http://%s:%d",
environment.getServiceHost("api", 8080),
environment.getServicePort("api", 8080));
String webUrl = String.format("http://%s:%d",
environment.getServiceHost("web", 80),
environment.getServicePort("web", 80));
// Run integration tests
testApiEndpoints(apiUrl);
testWebInterface(webUrl);
testEndToEndFlow(webUrl, apiUrl);
}
}
private void testApiEndpoints(String apiUrl) {
// Test API endpoints
}
private void testWebInterface(String webUrl) {
// Test frontend
}
private void testEndToEndFlow(String webUrl, String apiUrl) {
// Test complete user flow
}
}import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.File;
public class MicroservicesIntegrationTest {
@Test
public void testMicroservicesInteraction() {
// docker-compose.yml contains multiple microservices:
// - user-service
// - order-service
// - payment-service
// - notification-service
// - api-gateway
DockerComposeContainer<?> services = new DockerComposeContainer<>(
new File("microservices-compose.yml"))
// Expose each service
.withExposedService("user-service", 8081,
Wait.forHttp("/actuator/health").forStatusCode(200))
.withExposedService("order-service", 8082,
Wait.forHttp("/actuator/health").forStatusCode(200))
.withExposedService("payment-service", 8083,
Wait.forHttp("/actuator/health").forStatusCode(200))
.withExposedService("notification-service", 8084,
Wait.forHttp("/actuator/health").forStatusCode(200))
.withExposedService("api-gateway", 8080,
Wait.forHttp("/health").forStatusCode(200))
// Set test environment
.withEnv("SPRING_PROFILES_ACTIVE", "test")
.withPull(true)
.withBuild(true);
services.start();
// Test through API gateway
String gatewayUrl = String.format("http://%s:%d",
services.getServiceHost("api-gateway", 8080),
services.getServicePort("api-gateway", 8080));
// Test inter-service communication
testUserCreation(gatewayUrl);
testOrderPlacement(gatewayUrl);
testPaymentProcessing(gatewayUrl);
testNotificationDelivery(gatewayUrl);
services.stop();
}
}import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class LoadBalancingTest {
@Test
public void testLoadBalancedWorkers() {
// docker-compose.yml contains:
// - nginx (load balancer)
// - worker (application service)
// - redis (shared state)
DockerComposeContainer<?> cluster = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withExposedService("nginx", 80,
Wait.forHttp("/").forStatusCode(200))
.withExposedService("redis", 6379,
Wait.forListeningPort())
// Scale workers to 5 instances
.withScaledService("worker", 5)
.withExposedService("worker", 8080,
Wait.forHttp("/health").forStatusCode(200));
cluster.start();
// Get load balancer URL
String lbUrl = String.format("http://%s:%d",
cluster.getServiceHost("nginx", 80),
cluster.getServicePort("nginx", 80));
// Test that requests are distributed across workers
List<String> workerIds = new ArrayList<>();
for (int i = 0; i < 20; i++) {
String workerId = makeRequest(lbUrl + "/worker-id");
workerIds.add(workerId);
}
// Verify load balancing
long uniqueWorkers = workerIds.stream().distinct().count();
assertTrue(uniqueWorkers > 1, "Requests should be distributed");
cluster.stop();
}
}import org.testcontainers.containers.DockerComposeContainer;
import java.io.File;
import java.util.List;
public class DevelopmentEnvironmentTest {
@Test
public void testDevelopmentSetup() {
// Combine base and override files
DockerComposeContainer<?> devEnv = new DockerComposeContainer<>(
List.of(
new File("docker-compose.yml"), // Base configuration
new File("docker-compose.dev.yml") // Dev overrides
))
.withExposedService("app", 8080)
.withExposedService("db", 5432)
.withEnv("DEBUG", "true")
.withEnv("HOT_RELOAD", "true")
.withTailChildContainers(true); // See all container logs
devEnv.start();
// Development environment is ready
String appUrl = String.format("http://%s:%d",
devEnv.getServiceHost("app", 8080),
devEnv.getServicePort("app", 8080));
// Test with hot reload enabled
testWithCodeChanges(appUrl);
devEnv.stop();
}
}import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
public class DatabaseMigrationTest {
@Test
public void testMigrations() throws Exception {
// docker-compose.yml contains:
// - postgres (database)
// - flyway (migration tool)
DockerComposeContainer<?> dbSetup = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withExposedService("postgres", 5432,
Wait.forListeningPort())
.withExposedService("flyway", 0, // One-shot container
Wait.forLogMessage(".*Successfully applied.*", 1))
.withEnv("POSTGRES_DB", "testdb")
.withEnv("POSTGRES_USER", "testuser")
.withEnv("POSTGRES_PASSWORD", "testpass");
dbSetup.start();
// Wait for migrations to complete
Thread.sleep(5000);
// Connect to database
String jdbcUrl = String.format("jdbc:postgresql://%s:%d/testdb",
dbSetup.getServiceHost("postgres", 5432),
dbSetup.getServicePort("postgres", 5432));
try (Connection conn = DriverManager.getConnection(
jdbcUrl, "testuser", "testpass")) {
// Verify migrations
ResultSet rs = conn.createStatement().executeQuery(
"SELECT COUNT(*) FROM flyway_schema_history");
rs.next();
int migrationCount = rs.getInt(1);
assertTrue(migrationCount > 0, "Migrations should have run");
// Verify schema
rs = conn.createStatement().executeQuery(
"SELECT table_name FROM information_schema.tables " +
"WHERE table_schema = 'public'");
List<String> tables = new ArrayList<>();
while (rs.next()) {
tables.add(rs.getString(1));
}
assertTrue(tables.contains("users"), "Users table should exist");
assertTrue(tables.contains("orders"), "Orders table should exist");
}
dbSetup.stop();
}
}import org.testcontainers.containers.DockerComposeContainer;
import java.io.File;
public class CustomComposeOptionsTest {
@Test
public void testWithCustomOptions() {
DockerComposeContainer<?> custom = new DockerComposeContainer<>(
new File("docker-compose.yml"))
.withExposedService("app", 8080)
// Custom docker-compose options
.withOptions(
"--compatibility", // Enable compatibility mode
"--renew-anon-volumes" // Recreate anonymous volumes
)
.withPull(true) // Always pull latest images
.withBuild(true) // Build images before starting
.withRemoveImages(DockerComposeContainer.RemoveImages.LOCAL); // Clean up
custom.start();
// Test with custom configuration
String appUrl = String.format("http://%s:%d",
custom.getServiceHost("app", 8080),
custom.getServicePort("app", 8080));
// Run tests
// ...
custom.stop();
}
}# docker-compose.yml - Base configuration
version: '3.8'
services:
app:
build: .
ports:
- "8080"
depends_on:
- db
- redis
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
redis:
image: redis:7.0
# docker-compose.test.yml - Test overrides
version: '3.8'
services:
app:
environment:
- SPRING_PROFILES_ACTIVE=test
- LOG_LEVEL=DEBUG
build:
args:
- ENV=test
db:
environment:
- POSTGRES_PASSWORD=test// Choose appropriate wait strategies for each service type
// Databases - wait for port
.withExposedService("postgres", 5432,
Wait.forListeningPort())
// HTTP services - wait for health endpoint
.withExposedService("api", 8080,
Wait.forHttp("/health").forStatusCode(200))
// Services with specific startup messages
.withExposedService("kafka", 9092,
Wait.forLogMessage(".*started.*", 1))
// Services with healthcheck
.withExposedService("app", 8080,
Wait.forHealthcheck())// Always use try-with-resources
try (DockerComposeContainer<?> env = new DockerComposeContainer<>(...)) {
env.start();
// Use environment
} // Automatic cleanup
// Or manually manage lifecycle
DockerComposeContainer<?> env = new DockerComposeContainer<>(...);
try {
env.start();
// Use environment
} finally {
env.stop();
}
// Configure image removal
.withRemoveImages(RemoveImages.LOCAL) // Remove built images// Use unique environment variables per test
DockerComposeContainer<?> test1 = new DockerComposeContainer<>(...)
.withEnv("TEST_ID", "test1")
.withEnv("DB_NAME", "test1_db");
DockerComposeContainer<?> test2 = new DockerComposeContainer<>(...)
.withEnv("TEST_ID", "test2")
.withEnv("DB_NAME", "test2_db");
// Ensures tests don't interfere with each other