Strategies for determining if a container has started correctly and is ready for use. Startup check strategies operate independently of wait strategies, providing an alternative or complementary mechanism for validating container startup success. While wait strategies focus on determining when a container is ready for testing, startup check strategies verify that the container started without errors.
Base class for all startup check strategies, providing the framework for checking container startup status.
/**
* Strategy for determining whether a container has started correctly.
* Provides a mechanism to verify container startup success or failure
* independent of wait strategies.
*/
public abstract class StartupCheckStrategy {
/**
* Enum representing possible startup states.
*/
public enum StartupStatus {
/** Startup status not yet determined */
NOT_YET_KNOWN,
/** Container started successfully */
SUCCESSFUL,
/** Container failed to start */
FAILED
}
/**
* Check the current startup state without blocking.
* Must be implemented by subclasses.
*
* @param dockerClient the Docker client for querying container state
* @param containerId the container ID to check
* @return the current startup status
*/
public abstract StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
/**
* Wait until the container startup is successful or failed.
* Blocks until a definitive status is determined within the configured timeout.
*
* @param dockerClient the Docker client
* @param containerId the container ID
* @return true if startup successful, false if failed
*/
public boolean waitUntilStartupSuccessful(DockerClient dockerClient, String containerId);
/**
* Set the timeout for startup checks.
* Default timeout is 30 seconds (GenericContainer.CONTAINER_RUNNING_TIMEOUT_SEC).
*
* @param timeout the timeout duration
* @return this strategy with generic type for proper subclass chaining
*/
public <SELF extends StartupCheckStrategy> SELF withTimeout(Duration timeout);
/**
* Get the current Docker container state information.
* Protected utility method for subclasses.
*
* @param dockerClient the Docker client
* @param containerId the container ID
* @return the container state information
*/
protected InspectContainerResponse.ContainerState getCurrentState(DockerClient dockerClient, String containerId);
}Key Characteristics:
checkStartupState()Unreliables.retryUntilTrue()The simplest and most common startup check strategy. Verifies that the container is running and hasn't exited with a failure code.
/**
* Simplest possible startup check strategy.
* Verifies the container has reached the running state and has not exited.
* This is the default strategy for most containers.
*
* Success conditions:
* - Container is currently running, OR
* - Container has exited with exit code 0 (success)
*
* Failure conditions:
* - Container has exited with non-zero exit code
*/
public class IsRunningStartupCheckStrategy extends StartupCheckStrategy {
/**
* Check if the container is running or successfully exited.
*
* @param dockerClient the Docker client
* @param containerId the container ID
* @return SUCCESSFUL if running or exited with code 0,
* FAILED if exited with non-zero code,
* NOT_YET_KNOWN if still starting
*/
@Override
public StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
}When to Use:
Behavior:
Limitations:
WaitStrategy for application-level readiness checksUsage Example:
GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.0"))
.withExposedPorts(6379)
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofSeconds(60)));
redis.start();
// Container is running and startup check passedFor containers that run a single command and exit. Considers the container successfully started when it has exited with code 0.
/**
* Startup check strategy for one-shot/batch containers.
* Designed for containers that run briefly and exit of their own accord.
* Success is when the container has stopped with exit code 0.
*
* Success conditions:
* - Container has exited with exit code 0
*
* Failure conditions:
* - Container has exited with non-zero exit code
*
* Pending conditions:
* - Container is still running
*/
public class OneShotStartupCheckStrategy extends StartupCheckStrategy {
/**
* Check if the one-shot container has completed successfully.
*
* @param dockerClient the Docker client
* @param containerId the container ID
* @return SUCCESSFUL when container exits with code 0,
* FAILED when container exits with non-zero code,
* NOT_YET_KNOWN while container is still running
*/
@Override
public StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
}When to Use:
Behavior:
Key Differences from IsRunningStartupCheckStrategy:
Usage Example:
// Data import container that processes files and exits
GenericContainer<?> dataImporter = new GenericContainer<>(
DockerImageName.parse("myapp/data-importer:latest"))
.withCommand("/bin/sh", "-c", "python /scripts/import_data.py")
.withCopyToContainer(
Transferable.of("import_data.py"),
"/scripts/import_data.py"
)
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()
.withTimeout(Duration.ofMinutes(5)));
dataImporter.start();
// Wait for exit
String logs = dataImporter.getLogs();
if (dataImporter.getContainerInfo().getState().getExitCode() == 0) {
System.out.println("Data import successful");
}Extended one-shot strategy that waits indefinitely for the container to complete. Use when the execution time is unpredictable or can be very long.
/**
* One-shot startup check strategy with indefinite wait.
* Variant of OneShotStartupCheckStrategy that does not impose a timeout.
* Intended for situations where long-running tasks form part of container startup.
*
* Behavior:
* - Waits indefinitely for container to exit
* - Expects exit code 0 for success
* - No timeout constraints (use with care in CI/CD)
*
* Assumes the container will stop of its own accord,
* either with success or failure exit code.
*/
public class IndefiniteWaitOneShotStartupCheckStrategy extends OneShotStartupCheckStrategy {
/**
* Wait indefinitely for the one-shot container to complete.
* Polls the container state every 100ms without timeout.
*
* @param dockerClient the Docker client
* @param containerId the container ID
* @return true if container exits with code 0, false if non-zero exit code
*/
@Override
public boolean waitUntilStartupSuccessful(DockerClient dockerClient, String containerId);
}When to Use:
Behavior:
Warnings:
Usage Example:
// Long-running machine learning training job
GenericContainer<?> mlTrainer = new GenericContainer<>(
DockerImageName.parse("myapp/ml-trainer:latest"))
.withCommand("/bin/sh", "-c", "python /scripts/train_model.py --epochs 100")
.withCopyToContainer(
Transferable.of("train_model.py"),
"/scripts/train_model.py"
)
.withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy());
mlTrainer.start();
// Will wait indefinitely for training to complete
String trainingOutput = mlTrainer.getLogs();Ensures the container stays running for a minimum duration before being considered successfully started. Useful for detecting containers that start but crash immediately.
/**
* Startup check strategy that ensures the container runs for a minimum duration.
* Useful for detecting containers that start successfully but then fail.
* Prevents false positives where a container briefly starts then crashes.
*
* Success conditions:
* - Container is running AND
* - Container has been running for at least the specified minimum duration
*
* Failure conditions:
* - Container has stopped with non-zero exit code
* - Container exits before minimum duration is reached
*
* Pending conditions:
* - Container is running but hasn't reached minimum duration yet
*/
public class MinimumDurationRunningStartupCheckStrategy extends StartupCheckStrategy {
/**
* Constructor setting the minimum duration the container must run.
*
* @param minimumRunningDuration the minimum duration (must not be null)
* @throws NullPointerException if minimumRunningDuration is null
*/
public MinimumDurationRunningStartupCheckStrategy(
@NotNull Duration minimumRunningDuration
);
/**
* Check if the container has been running for the minimum duration.
*
* @param dockerClient the Docker client
* @param containerId the container ID
* @return SUCCESSFUL if running for minimum duration,
* FAILED if container has stopped,
* NOT_YET_KNOWN if running but minimum duration not yet reached
*/
@Override
public StartupStatus checkStartupState(DockerClient dockerClient, String containerId);
}When to Use:
Behavior:
Key Advantages:
Typical Duration Values:
// Brief stability check (good for fast-starting apps)
Duration.ofSeconds(5)
// Standard stability check
Duration.ofSeconds(10)
// Conservative stability check (for slower apps)
Duration.ofSeconds(30)
// Very conservative check
Duration.ofSeconds(60)Usage Example:
// Microservice that should prove it can run stably
GenericContainer<?> microservice = new GenericContainer<>(
DockerImageName.parse("mycompany/api-service:latest"))
.withExposedPorts(8080)
.withEnv("JAVA_OPTS", "-Xmx256m")
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(10))
);
microservice.start();
// Container must have been running successfully for 10 seconds
// Verify the service is actually responding
String response = HTTP_CLIENT.get(String.format("http://%s:%d/health",
microservice.getHost(),
microservice.getMappedPort(8080)));Key differences and complementary usage:
| Aspect | Startup Check | Wait Strategy |
|---|---|---|
| Purpose | Verify container started correctly | Verify container is ready for testing |
| Timing | Runs immediately after container.start() | Runs after startup check passes |
| Level | Container state (running/stopped) | Application level (port, HTTP, logs) |
| Timeout | Container startup timeout (default 30s) | Configurable per strategy |
| Example | "Container is running" | "Application responds to HTTP requests" |
| Used for | Catching startup failures | Ensuring application readiness |
Complementary Usage:
GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("webapp:latest"))
.withExposedPorts(8080)
// Startup check: ensure container stays running for 5 seconds
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(5))
)
// Wait strategy: ensure application is responding to requests
.waitingFor(Wait.forHttp("/health")
.forStatusCode(200)
.withStartupTimeout(Duration.ofSeconds(60)));
app.start();
// Both checks have passed: container is stable AND application is respondingUsing startup check strategies in containers:
/**
* Set the startup check strategy for the container.
* Determines how the container startup success is verified.
*
* @param strategy the startup check strategy to use
* @return this container for method chaining
*/
public SELF withStartupCheckStrategy(StartupCheckStrategy strategy);
/**
* Set the minimum running duration for container startup.
* Convenience method that creates a MinimumDurationRunningStartupCheckStrategy.
*
* @param minimumRunningDuration the minimum duration the container must run
* @return this container for method chaining
*/
public SELF withMinimumRunningDuration(Duration minimumRunningDuration);
/**
* Set the number of startup attempts before giving up.
* Container will be restarted this many times if startup fails.
*
* @param attempts the number of startup attempts
* @return this container for method chaining
*/
public SELF withStartupAttempts(int attempts);
/**
* Set the startup timeout for the startup check.
*
* @param timeout the timeout duration
* @return this container for method chaining
*/
public SELF withStartupTimeout(Duration timeout);Usage Examples:
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
// Using IsRunningStartupCheckStrategy
GenericContainer<?> database = new GenericContainer<>(DockerImageName.parse("postgres:15"))
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "secret")
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofSeconds(60)));
// Using OneShotStartupCheckStrategy
GenericContainer<?> batchJob = new GenericContainer<>(DockerImageName.parse("myapp/job:latest"))
.withCommand("sh", "-c", "python process.py")
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()
.withTimeout(Duration.ofMinutes(5)));
// Using MinimumDurationRunningStartupCheckStrategy
GenericContainer<?> stableService = new GenericContainer<>(
DockerImageName.parse("myapp/service:latest"))
.withExposedPorts(8080)
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(10))
);
// Using convenience method
GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("app:latest"))
.withExposedPorts(8080)
.withMinimumRunningDuration(Duration.ofSeconds(10));
// Using with startup attempts
GenericContainer<?> flaky = new GenericContainer<>(DockerImageName.parse("flaky-app:latest"))
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofSeconds(30)))
.withStartupAttempts(3); // Retry up to 3 times if startup fails
flaky.start();| Container Type | Startup Check | Rationale |
|---|---|---|
| Long-running services (API, database, cache) | IsRunningStartupCheckStrategy | Should remain running indefinitely |
| Batch jobs, data import, ETL | OneShotStartupCheckStrategy | Runs once and exits |
| Long-running batch job (unpredictable duration) | IndefiniteWaitOneShotStartupCheckStrategy | No timeout needed |
| Service with crash-immediately bugs | MinimumDurationRunningStartupCheckStrategy | Needs to prove stability |
| Most applications | IsRunningStartupCheckStrategy (default) | Safe default choice |
Is the container expected to stay running after startup?
├─ YES → IsRunningStartupCheckStrategy
│ └─ Does it crash immediately on config errors?
│ └─ YES → MinimumDurationRunningStartupCheckStrategy (with stability check)
└─ NO → Does it run for a predictable time?
├─ YES → OneShotStartupCheckStrategy
└─ NO → IndefiniteWaitOneShotStartupCheckStrategyGenericContainer<?> webApp = new GenericContainer<>(
DockerImageName.parse("mycompany/api:latest"))
.withExposedPorts(8080)
.withEnv("ENVIRONMENT", "test")
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofSeconds(45)))
.waitingFor(Wait.forHttp("/health")
.forStatusCode(200)
.withStartupTimeout(Duration.ofSeconds(30)));
webApp.start();
// Startup check: container is running
// Wait strategy: HTTP /health endpoint responds with 200GenericContainer<?> appWithMigration = new GenericContainer<>(
DockerImageName.parse("myapp/migrator:latest"))
.withExposedPorts(8080)
.withEnv("DB_HOST", "db.example.com")
// Allow extra time for database migration
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofMinutes(2)))
.waitingFor(Wait.forLogMessage(".*Migration completed.*", 1)
.withStartupTimeout(Duration.ofMinutes(2)));
appWithMigration.start();GenericContainer<?> dataProcessor = new GenericContainer<>(
DockerImageName.parse("myapp/processor:latest"))
.withCommand("sh", "-c", "python process_data.py input.csv output.csv")
.withCopyToContainer(Transferable.of(csvData), "/data/input.csv")
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()
.withTimeout(Duration.ofMinutes(10)));
dataProcessor.start();
String output = dataProcessor.getLogs();
String processedData = dataProcessor.copyFileFromContainer("/data/output.csv");GenericContainer<?> service = new GenericContainer<>(
DockerImageName.parse("myapp/service:latest"))
.withExposedPorts(8080)
.withEnv("CONFIGURATION_REQUIRED", "true") // Will crash if not set
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(15))
);
try {
service.start();
// If we reach here, container has been running for 15+ seconds
// indicating configuration was valid
} catch (ContainerLaunchException e) {
// Configuration error caused immediate crash
System.err.println("Service configuration invalid:");
System.err.println(service.getLogs());
}GenericContainer<?> trainer = new GenericContainer<>(
DockerImageName.parse("myapp/ml-trainer:latest"))
.withCommand("python", "/app/train.py", "--epochs", "100")
.withEnv("CUDA_VISIBLE_DEVICES", "0")
.withStartupCheckStrategy(
new IndefiniteWaitOneShotStartupCheckStrategy()
);
System.out.println("Starting model training (may take hours)...");
trainer.start();
String trainingLogs = trainer.getLogs();
System.out.println("Training completed:");
System.out.println(trainingLogs);
// Extract trained model
trainer.copyFileFromContainer("/models/trained.pkl", "./trained_model.pkl");GenericContainer<?> complexApp = new GenericContainer<>(
DockerImageName.parse("complex-app:latest"))
.withExposedPorts(8080, 9090)
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(10))
)
.waitingFor(new WaitAllStrategy()
.withStrategy(Wait.forListeningPort())
.withStrategy(Wait.forHttp("/ready").forStatusCode(200))
.withStrategy(Wait.forLogMessage(".*Initialization complete.*", 1))
.withStartupTimeout(Duration.ofSeconds(60)));
complexApp.start();GenericContainer<?> unreliableService = new GenericContainer<>(
DockerImageName.parse("unreliable-service:latest"))
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofSeconds(30)))
.withStartupAttempts(3); // Retry up to 3 times
try {
unreliableService.start();
} catch (ContainerLaunchException e) {
System.err.println("Service failed to start after 3 attempts");
}StartupCheckStrategy startupCheck = System.getenv("QUICK_START") != null
? new IsRunningStartupCheckStrategy().withTimeout(Duration.ofSeconds(10))
: new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(30));
GenericContainer<?> conditionalApp = new GenericContainer<>(
DockerImageName.parse("app:latest"))
.withExposedPorts(8080)
.withStartupCheckStrategy(startupCheck);Symptoms:
org.testcontainers.containers.ContainerLaunchException:
Container startup failedCommon Causes:
Timeout too short for application startup
// Solution: Increase timeout
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofSeconds(120)))Container crashes immediately (with OneShotStartupCheckStrategy)
// Check logs to diagnose crash
System.err.println(container.getLogs());Wrong strategy for container type
// Problem: Using OneShotStartupCheckStrategy for long-running service
// Solution: Use IsRunningStartupCheckStrategy instead
.withStartupCheckStrategy(new IsRunningStartupCheckStrategy())Symptoms: Container appears to start but crashes after a few seconds
Solution: Use MinimumDurationRunningStartupCheckStrategy
GenericContainer<?> app = new GenericContainer<>(DockerImageName.parse("app:latest"))
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(20))
);
app.start();
// If we reach here, container has been running stably for 20+ secondsSymptoms: Data processing job times out before completion
Solution: Use IndefiniteWaitOneShotStartupCheckStrategy
// Problem: Job takes 2 hours, timeout at 5 minutes
.withStartupCheckStrategy(new OneShotStartupCheckStrategy()
.withTimeout(Duration.ofMinutes(5)))
// Solution: Wait indefinitely
.withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())Best Practice: Capture logs on startup failure
try {
container.start();
} catch (ContainerLaunchException e) {
System.err.println("Container startup failed");
System.err.println("Startup check strategy: " +
container.getStartupCheckStrategy().getClass().getSimpleName());
System.err.println("Container logs:");
System.err.println(container.getLogs());
System.err.println("Container status:");
System.err.println(container.getCurrentContainerInfo().getState());
throw e;
}import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.utility.DockerImageName;
import java.time.Duration;
public class CompleteStartupCheckExample {
public void testComplexSystem() {
Network network = Network.newNetwork();
// Database with startup check
GenericContainer<?> database = new GenericContainer<>(
DockerImageName.parse("postgres:15"))
.withNetwork(network)
.withNetworkAliases("database")
.withExposedPorts(5432)
.withEnv("POSTGRES_PASSWORD", "testpass")
.withStartupCheckStrategy(
new IsRunningStartupCheckStrategy()
.withTimeout(Duration.ofSeconds(60))
)
.waitingFor(Wait.forHealthcheck()
.withStartupTimeout(Duration.ofSeconds(60)));
// Cache service with minimum duration check
GenericContainer<?> cache = new GenericContainer<>(
DockerImageName.parse("redis:7.0"))
.withNetwork(network)
.withNetworkAliases("cache")
.withExposedPorts(6379)
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(5))
)
.waitingFor(Wait.forListeningPort());
// Main application with comprehensive checks
WaitAllStrategy appWaitStrategy = new WaitAllStrategy()
.withStrategy(Wait.forListeningPort())
.withStrategy(Wait.forHttp("/health")
.forStatusCode(200)
.forResponsePredicate(r -> r.contains("\"status\":\"UP\"")))
.withStartupTimeout(Duration.ofSeconds(60));
GenericContainer<?> app = new GenericContainer<>(
DockerImageName.parse("myapp/service:latest"))
.withNetwork(network)
.withExposedPorts(8080)
.withEnv("DB_HOST", "database")
.withEnv("CACHE_HOST", "cache")
.dependsOn(database, cache)
.withStartupCheckStrategy(
new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(10))
)
.waitingFor(appWaitStrategy);
// Start all containers
try {
database.start();
System.out.println("Database started successfully");
cache.start();
System.out.println("Cache started successfully");
app.start();
System.out.println("Application started successfully");
// All checks have passed - system is ready
String appUrl = String.format("http://%s:%d",
app.getHost(),
app.getMappedPort(8080));
System.out.println("System ready at: " + appUrl);
// Run tests...
} catch (Exception e) {
System.err.println("System startup failed:");
System.err.println("Database logs: " + database.getLogs());
System.err.println("Cache logs: " + cache.getLogs());
System.err.println("App logs: " + app.getLogs());
throw new RuntimeException("Failed to start test system", e);
}
}
}| Strategy | Overhead | Best For |
|---|---|---|
IsRunningStartupCheckStrategy | Minimal (~100ms) | Fast-starting services |
OneShotStartupCheckStrategy | Variable (depends on job) | Batch operations |
IndefiniteWaitOneShotStartupCheckStrategy | Variable (no timeout) | Long-running jobs |
MinimumDurationRunningStartupCheckStrategy | Depends on duration (adds ~duration overhead) | Stability-critical services |
Use IsRunningStartupCheckStrategy as default
Set appropriate timeouts
// Too long - wastes time
.withTimeout(Duration.ofMinutes(5))
// Appropriate for typical service
.withTimeout(Duration.ofSeconds(30))Combine with appropriate wait strategy
// Startup check verifies container stability
.withStartupCheckStrategy(new MinimumDurationRunningStartupCheckStrategy(Duration.ofSeconds(5)))
// Wait strategy verifies application readiness
.waitingFor(Wait.forHttp("/ready").forStatusCode(200))Consider container reuse in development
container.withReuse(true); // Requires testcontainers.reuse.enable=truewait-strategies.md) - Application-level readiness checkingcontainer-lifecycle.md) - Container configuration and lifecycle managementnetwork-configuration.md) - Multi-container networking