Skip to content

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:

  1. Large image size: MSSQL images are 1.5-1.8GB. Compare that to PostgreSQL (~400MB) or MySQL (~500MB).

  2. Long initialization: The database engine needs time to start system databases, allocate memory, and bind to ports.

  3. 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:

Performance Impact Table
| 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

BeforeReuse.java
@Container
static MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>()
.withEnv("ACCEPT_EULA", "Y");

After: Container is reused across test runs

AfterReuse.java
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.properties
testcontainers.reuse.enable=true

Place 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.

AbstractContainerBaseTest.java
@SpringBootTest
@Testcontainers
public 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:

UserRepositoryTest.java
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

InefficientWait.java
@Container
static MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>()
.withEnv("ACCEPT_EULA", "Y")
.waitingFor(Wait.forListeningPort()); // Port open ≠ database ready

Good: Uses container’s health status

OptimizedWait.java
@Container
static MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>()
.withEnv("ACCEPT_EULA", "Y")
.waitingFor(Wait.forHealthcheck()
.withStartupTimeout(Duration.ofSeconds(90)));

Here’s a comparison:

Wait Strategy 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.

github-actions.yml
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 test

CI 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:

AbstractContainerBaseTest.java
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
@Testcontainers
public 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:

pom.xml
<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=true is in src/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

  1. Add testcontainers.reuse.enable=true to properties
  2. Add withReuse(true) to container definition
  3. Call start() manually
  4. Create abstract base test class with singleton container
  5. Configure Spring Boot with @DynamicPropertySource
  6. Replace forListeningPort() with forHealthcheck()
  7. Add pre-pull step to CI config

Key Takeaways

  1. Container reuse is the most impactful change. Enable withReuse(true) for immediate 95% improvement after the first run.

  2. Singleton pattern prevents multiple container startups. One container serves all test classes.

  3. Healthcheck waits are more reliable than port-based waits. The database is actually ready when tests start.

  4. 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