or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

container-configuration.mdcontainer-customization.mdcontainer-lifecycle.mddatabase-initialization.mdindex.mdjdbc-connection.mdr2dbc-support.mdurl-parameters-timeouts.md
tile.json

container-lifecycle.mddocs/

Container Lifecycle Management

This document covers controlling MySQL container startup, shutdown, and monitoring container state.

Quick Reference

Lifecycle Methods:

  • start(): void - Starts container and blocks until ready (throws ContainerLaunchException or IllegalStateException)
  • stop(): void - Stops and removes container
  • isRunning(): boolean - Returns boolean indicating if container is running

Configuration:

  • withReuse(boolean reuse): SELF - Enable container reuse (requires testcontainers.reuse.enable=true in ~/.testcontainers.properties)

AutoCloseable Support:

  • MySQLContainer implements AutoCloseable - Use with try-with-resources for automatic cleanup

State Transitions:

  • Before start(): isRunning() returns false
  • After start() completes: isRunning() returns true
  • After stop(): isRunning() returns false
  • After exception during start(): isRunning() returns false

Capabilities

Start Container

Start the MySQL container and wait until it's ready to accept connections.

/**
 * Starts the MySQL container.
 * This method pulls the Docker image if not already present, creates the container,
 * starts it, and waits until the database is ready to accept connections.
 *
 * The method blocks until the container is fully started and the test query succeeds,
 * or until the startup timeout is reached.
 *
 * This method can only be called once per container instance. Calling it multiple
 * times will throw an IllegalStateException.
 *
 * All configuration methods must be called before this method.
 *
 * @throws IllegalStateException if the container cannot be started or accessed within the timeout,
 *         or if start() is called multiple times, or if configuration is invalid
 * @throws ContainerLaunchException if the container fails to start due to configuration issues
 *         (e.g., empty password with non-root user, invalid Docker image, insufficient resources)
 */
void start();

Usage Example:

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"));
mysql.start(); // Blocks until container is ready

// Container is now running and ready for connections
String jdbcUrl = mysql.getJdbcUrl();

Startup Process:

  1. Docker image is pulled if not already present locally
  2. Container is created with configured settings
  3. Container is started
  4. Liveness check begins (executes test query: "SELECT 1")
  5. Initialization scripts are executed (if configured)
  6. Container is marked as ready when all checks pass

Startup Timeout:

  • Default: 120 seconds
  • Configurable via withStartupTimeoutSeconds(int)
  • Includes Docker image pull time if image not cached
  • Throws IllegalStateException if timeout exceeded
  • Timeout applies to entire startup process including image pull

Startup Failure Scenarios:

  • Docker image not found or cannot be pulled
  • Insufficient Docker resources (memory, CPU, disk)
  • Port conflicts (rare, as ports are dynamically assigned)
  • Invalid configuration (empty password with non-root user)
  • Network connectivity issues
  • Container internal failures

Stop Container

Stop and remove the MySQL container.

/**
 * Stops the MySQL container.
 * This method stops the running container and removes it, including any volumes.
 * Any data in the container is lost unless persisted externally.
 *
 * This method is idempotent: calling it multiple times or on an already-stopped
 * container has no effect and does not throw exceptions.
 *
 * After calling stop(), the container cannot be restarted. Create a new instance
 * if you need another container.
 */
void stop();

Usage Example:

MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"));
mysql.start();

// Use container...

mysql.stop(); // Stops and removes the container

Stop Behavior:

  • The container is stopped gracefully
  • The container is removed after stopping
  • Any data stored in the container is lost
  • Network connections are closed
  • No exceptions thrown if container is already stopped
  • Method is idempotent (safe to call multiple times)

Check Running State

Check whether the container is currently running.

/**
 * Checks if the container is currently running.
 * Returns true only if the container has been successfully started and has not
 * been stopped. Returns false before start() is called, after stop() is called,
 * or if start() failed with an exception.
 *
 * @return true if the container is running, false otherwise
 */
boolean isRunning();

Usage Example:

MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"));

boolean running1 = mysql.isRunning(); // false (not started yet)

mysql.start();
boolean running2 = mysql.isRunning(); // true (running)

mysql.stop();
boolean running3 = mysql.isRunning(); // false (stopped)

State Transitions:

  • Before start(): isRunning() returns false
  • After start() completes: isRunning() returns true
  • After stop(): isRunning() returns false
  • After exception during start(): isRunning() returns false

State Checking Pattern:

if (mysql.isRunning()) {
    // Safe to use container
    String jdbcUrl = mysql.getJdbcUrl();
} else {
    // Container not ready, must call start() first
    mysql.start();
}

Container Reuse

Enable container reuse across multiple test runs to improve performance.

/**
 * Enables or disables container reuse.
 * When enabled, the container will not be stopped and removed after the test completes.
 * Subsequent test runs can reuse the same container instance, significantly improving startup time.
 *
 * Requires Testcontainers reuse feature to be enabled in ~/.testcontainers.properties:
 * testcontainers.reuse.enable=true
 *
 * If reuse is enabled but the property file is not configured, the container will still
 * be stopped and removed after tests complete.
 *
 * @param reuse true to enable reuse, false to disable (default: false)
 * @return self for method chaining (SELF extends MySQLContainer<SELF>)
 */
SELF withReuse(boolean reuse);

Usage Example:

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
    .withDatabaseName("testdb")
    .withReuse(true); // Enable reuse

mysql.start();
// First run: container is created and started
// Subsequent runs: existing container is reused if still running

Reuse Configuration:

To enable container reuse, add this line to ~/.testcontainers.properties:

testcontainers.reuse.enable=true

Reuse Behavior:

  • Containers are identified by their configuration hash
  • If a compatible container is already running, it will be reused
  • If no compatible container exists, a new one is created
  • Reused containers persist across JVM restarts
  • Manual cleanup may be needed: docker ps -a --filter label=org.testcontainers.reuse.enable=true
  • Data persists in reused containers between test runs
  • Use with caution: ensure tests clean up data or use separate databases

When to Use Reuse:

  • Development environments for faster test iteration
  • Long-running test suites where startup time is significant
  • Tests that don't modify database state
  • Local development only

When NOT to Use Reuse:

  • CI/CD pipelines (ensure clean state)
  • Tests that modify database state and expect clean slate
  • Tests that require specific container state
  • Production-like testing scenarios

Try-With-Resources Pattern (Recommended)

The recommended way to manage container lifecycle is using try-with-resources, which ensures proper cleanup.

// MySQLContainer implements AutoCloseable
// close() method is called automatically when exiting try block

Usage Example:

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
import java.sql.Connection;
import java.sql.SQLException;

public void testWithAutoClose() throws SQLException {
    try (MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))) {
        mysql.start();

        // Use container
        try (Connection conn = mysql.createConnection("")) {
            // Execute queries...
        }

        // Container is automatically stopped and removed when exiting try block
    }
}

Benefits:

  • Automatic cleanup even if exceptions occur
  • No need to manually call stop()
  • Prevents container leaks in test suites
  • Cleaner, more readable code
  • Guaranteed cleanup in all code paths

Exception Handling:

try (MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))) {
    mysql.start();
    // If exception occurs here, container is still cleaned up
    throw new RuntimeException("Test failure");
} // Container is stopped here even if exception was thrown

JUnit Integration

JUnit 5 (Jupiter)

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

@Testcontainers
public class MySQLJUnit5Test {

    @Container
    private static MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
        .withDatabaseName("testdb");

    @Test
    public void testDatabase() {
        // Container is automatically started before tests and stopped after
        String jdbcUrl = mysql.getJdbcUrl();
        // Run test...
    }
}

JUnit 5 Behavior:

  • @Container with static field: Container started once before all tests, stopped after all tests
  • @Container with instance field: Container started before each test, stopped after each test
  • @Testcontainers annotation enables automatic lifecycle management
  • Container is stopped even if tests fail
  • No need to call start() or stop() manually

JUnit 4

import org.junit.ClassRule;
import org.junit.Test;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

public class MySQLJUnit4Test {

    @ClassRule
    public static MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
        .withDatabaseName("testdb");

    @Test
    public void testDatabase() {
        // Container is automatically started before tests and stopped after
        String jdbcUrl = mysql.getJdbcUrl();
        // Run test...
    }
}

JUnit 4 Behavior:

  • @ClassRule: Container started once before all tests, stopped after all tests
  • @Rule: Container started before each test, stopped after each test
  • Container is stopped even if tests fail
  • No need to call start() or stop() manually

Manual Lifecycle Management

For more control over container lifecycle:

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

public class ManualLifecycleTest {

    private MySQLContainer<?> mysql;

    public void setUp() {
        mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
            .withDatabaseName("testdb");
        mysql.start();
    }

    public void tearDown() {
        if (mysql != null && mysql.isRunning()) {
            mysql.stop();
        }
    }

    public void runTest() {
        // Use container
        String jdbcUrl = mysql.getJdbcUrl();
    }
}

Manual Management Notes:

  • Must explicitly call start() and stop()
  • Must handle exceptions and ensure cleanup
  • Check isRunning() before calling stop() to avoid errors
  • More error-prone than try-with-resources or JUnit integration
  • Use only when framework integration is not available

Shared Container Pattern

Share a single container across multiple test classes to improve performance:

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

public abstract class AbstractMySQLTest {

    protected static final MySQLContainer<?> MYSQL_CONTAINER;

    static {
        MYSQL_CONTAINER = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
            .withDatabaseName("testdb")
            .withReuse(true);
        MYSQL_CONTAINER.start();
    }

    protected String getJdbcUrl() {
        return MYSQL_CONTAINER.getJdbcUrl();
    }

    protected String getUsername() {
        return MYSQL_CONTAINER.getUsername();
    }

    protected String getPassword() {
        return MYSQL_CONTAINER.getPassword();
    }
}

// Test classes extend AbstractMySQLTest
public class UserRepositoryTest extends AbstractMySQLTest {
    // Tests use shared container via getJdbcUrl(), etc.
}

public class OrderRepositoryTest extends AbstractMySQLTest {
    // Tests use shared container via getJdbcUrl(), etc.
}

Shared Container Considerations:

  • Container started once in static initializer
  • All test classes share the same container instance
  • Tests must clean up data between runs or use transactions
  • Container persists for entire test suite execution
  • Use withReuse(true) for persistence across JVM restarts
  • Consider data isolation strategies (separate databases, transactions, cleanup)

Container Startup Timeout Configuration

Configure how long to wait for the container to become ready:

import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
    .withStartupTimeoutSeconds(180); // Wait up to 3 minutes (default: 120)

mysql.start();

Timeout Scenarios:

  • Slow network: Increase timeout for Docker image pulls
  • CI environments: Increase timeout for resource-constrained runners
  • Large initialization scripts: Increase timeout for script execution
  • First run: Increase timeout if Docker images not cached

Best Practices

  1. Use Try-With-Resources: Ensures containers are always cleaned up
  2. Enable Reuse for Development: Speeds up test iteration during development
  3. Disable Reuse for CI: Ensures clean state in CI/CD pipelines
  4. Share Containers Wisely: Share containers across test classes when tests don't modify database state
  5. Clean Database Between Tests: When sharing containers, clean/reset data between tests
  6. Monitor Resource Usage: Limit concurrent containers to avoid resource exhaustion
  7. Use Appropriate Timeouts: Increase timeouts for slow environments (CI, limited resources)
  8. Handle Exceptions: Ensure cleanup happens even when exceptions occur
  9. Use JUnit Integration: Leverage framework support for automatic lifecycle management
  10. Check State Before Use: Verify isRunning() before using container if lifecycle is managed manually

Error Handling

IllegalStateException: Thrown when the container starts but cannot be accessed via JDBC within the timeout period. This typically indicates:

  • Network connectivity issues
  • Firewall blocking container ports
  • Container internal failures
  • Insufficient resources (memory, CPU)
  • Database not ready within timeout

Solution:

// Increase timeout
mysql.withStartupTimeoutSeconds(300)
     .withConnectTimeoutSeconds(180);

// Enable logging to diagnose
mysql.withLogConsumer(new Slf4jLogConsumer(logger));

ContainerLaunchException: Thrown when the container fails to start due to configuration issues, such as:

  • Empty password with non-root user
  • Invalid Docker image name
  • Insufficient Docker resources
  • Port conflicts
  • Invalid configuration override path

Solution:

// Check configuration
mysql.withUsername("root")
     .withPassword("")
     .withEnv("MYSQL_ROOT_HOST", "%"); // Required for root with empty password

// Verify Docker resources
// Check Docker logs: docker logs <container-id>

To diagnose startup issues, enable container log output:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.utility.DockerImageName;

Logger logger = LoggerFactory.getLogger(getClass());

MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
    .withLogConsumer(new Slf4jLogConsumer(logger));

mysql.start();
// Container logs will be output via SLF4J