How to Speed Up Testcontainers MSSQL Startup
My tests were taking forever. Every time I ran my test suite, I had to wait 45 seconds just for MSSQL to start up in a container. Multiply that by 10 test classes, and I was spending 8 minutes waiting for containers. Something had to change.
The Problem with MSSQL Containers
MSSQL containers are notoriously slow to start. A developer on Reddit put it bluntly: “We use TestContainers to boot up a MSSQL server image, but it’s slow AF.”
I felt that pain. Here’s why MSSQL is slow:
-
Large image size: MSSQL images are 1.5-1.8GB. Compare that to PostgreSQL (~400MB) or MySQL (~500MB).
-
Long initialization: The database engine needs time to start system databases, allocate memory, and bind to ports.
-
Per-test lifecycle: If you start a fresh container for each test class, you multiply the startup time.
Here’s what my test suite looked like before optimization:
| Scenario | Startup Time | Test Suite Impact ||------------------------|--------------|-------------------|| Cold start, no reuse | 30-60 sec | Major bottleneck || Per-test container | 30-60 sec x N| Huge waste || With reuse enabled | 0-2 sec | Minimal impact |Solution #1: Enable Container Reuse
The single most impactful change I made was enabling container reuse. This keeps the container running between test runs.
Before: Container starts fresh each time
@Containerstatic MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>() .withEnv("ACCEPT_EULA", "Y");After: Container is reused across test runs
static MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>() .withEnv("ACCEPT_EULA", "Y") .withReuse(true); // Key optimization
static { mssql.start(); // Manual start required with reuse}You also need to add this to your configuration:
testcontainers.reuse.enable=truePlace this file in src/test/resources/.
Results:
- First run: 45 seconds (normal startup)
- Subsequent runs: 2 seconds (container already running)
That’s a 95% improvement after the first run.
Important: When using withReuse(true), you must call start() manually. Don’t call stop() - the container persists for reuse.
Solution #2: Singleton Container Pattern
If you have multiple test classes, don’t start a container for each one. Use a singleton pattern to share one container across all tests.
@SpringBootTest@Testcontainerspublic abstract class AbstractContainerBaseTest {
@Container protected static final MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>() .withEnv("ACCEPT_EULA", "Y") .withReuse(true);
@DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", mssql::getJdbcUrl); registry.add("spring.datasource.username", mssql::getUsername); registry.add("spring.datasource.password", mssql::getPassword); }}Now all your test classes extend this base:
class UserRepositoryTest extends AbstractContainerBaseTest {
@Test void testUserCreation() { // Container is already running // Tests execute immediately }}Performance gain:
- 10 test classes without singleton: 10 x 45s = 450 seconds
- 10 test classes with singleton: 1 x 45s = 45 seconds
That’s 7 minutes saved per test suite run.
Solution #3: Optimize Wait Strategies
The default wait strategy might not be optimal. I switched from port-based waiting to healthcheck-based waiting.
Bad: Only checks if port is open
@Containerstatic MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>() .withEnv("ACCEPT_EULA", "Y") .waitingFor(Wait.forListeningPort()); // Port open ≠ database readyGood: Uses container’s health status
@Containerstatic MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>() .withEnv("ACCEPT_EULA", "Y") .waitingFor(Wait.forHealthcheck() .withStartupTimeout(Duration.ofSeconds(90)));Here’s a comparison:
| Strategy | Time | Reliability ||-------------------|---------|-------------|| Fixed sleep | 60s+ | Low || Port listener | 15-20s | Medium || Healthcheck | 20-30s | High || Custom log message| 15-25s | High |Solution #4: CI/CD Pipeline Optimization
In CI, I pre-pull the MSSQL image before tests run. This saves download time.
name: Test Suite
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v3
# Pre-pull MSSQL image - name: Pull MSSQL Image run: docker pull mcr.microsoft.com/mssql/server:2019-latest
# Cache Testcontainers - name: Cache Testcontainers uses: actions/cache@v3 with: path: /tmp/testcontainers key: testcontainers-${{ runner.os }}
- name: Run Tests run: ./mvnw testCI performance impact:
- Without pre-pull: 45-60s (image pull + startup)
- With pre-pull: 20-30s (startup only)
Complete Working Example
Here’s my complete setup:
import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.DynamicPropertyRegistry;import org.springframework.test.context.DynamicPropertySource;import org.testcontainers.containers.MSSQLServerContainer;import org.testcontainers.junit.jupiter.Container;import org.testcontainers.junit.jupiter.Testcontainers;import org.testcontainers.utility.DockerImageName;import java.time.Duration;
@SpringBootTest@Testcontainerspublic abstract class AbstractContainerBaseTest {
@Container protected static final MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>( DockerImageName.parse("mcr.microsoft.com/mssql/server:2019-latest") ) .withEnv("ACCEPT_EULA", "Y") .withReuse(true) .waitingFor(Wait.forHealthcheck() .withStartupTimeout(Duration.ofSeconds(90)));
@DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", mssql::getJdbcUrl); registry.add("spring.datasource.username", mssql::getUsername); registry.add("spring.datasource.password", mssql::getPassword); registry.add("spring.datasource.driver-class-name", () -> "com.microsoft.sqlserver.jdbc.SQLServerDriver"); }}Maven dependencies:
<dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.19.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>mssqlserver</artifactId> <version>1.19.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.19.3</version> <scope>test</scope> </dependency> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>12.4.0.jre11</version> </dependency></dependencies>Performance Results
Here are my actual measurements:
Before optimization:
- Cold startup: 45 seconds
- Test suite (10 classes): 8 minutes
- CI pipeline: 12 minutes
With reuse + singleton:
- First run: 45 seconds
- Subsequent runs: 2 seconds
- Test suite: 2.5 minutes
- Improvement: 69% faster
Full optimization (reuse + singleton + healthcheck + pre-pull):
- Test suite: 2 minutes
- Improvement: 75% faster
Common Pitfalls
Container not reused?
- Check that
testcontainers.reuse.enable=trueis insrc/test/resources/testcontainers.properties
Tests fail with reuse?
- Your tests might be leaving dirty data. Add cleanup between tests or use
@DirtiesContext.
Healthcheck timeout?
- Increase the timeout:
withStartupTimeout(Duration.ofSeconds(120))
Port conflicts?
- Use dynamic ports (the default) or clean up previous containers manually.
Implementation Checklist
- Add
testcontainers.reuse.enable=trueto properties - Add
withReuse(true)to container definition - Call
start()manually - Create abstract base test class with singleton container
- Configure Spring Boot with
@DynamicPropertySource - Replace
forListeningPort()withforHealthcheck() - Add pre-pull step to CI config
Key Takeaways
-
Container reuse is the most impactful change. Enable
withReuse(true)for immediate 95% improvement after the first run. -
Singleton pattern prevents multiple container startups. One container serves all test classes.
-
Healthcheck waits are more reliable than port-based waits. The database is actually ready when tests start.
-
CI pre-pull saves image download time in pipelines.
MSSQL’s slow startup is a known challenge. But with proper Testcontainers configuration, my tests went from taking 8 minutes to 2 minutes. The key shift: start once, reuse everywhere.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments