Making Testcontainers Work with Lima on macOS

I switched from Docker Desktop to Lima a while back. Lighter, faster, no licensing headaches. For day-to-day Docker use it is seamless. Then I added Testcontainers to a Spring Boot project and the integration test started failing with the least helpful error message in the ecosystem:

ERROR tc.testcontainers/ryuk:0.11.0 -- Could not start container
ERROR tc.testcontainers/ryuk:0.11.0 -- There are no stdout/stderr logs available

No stack trace. No connection refused. Just silence and a 60-second timeout. This post is the guide I wish I had found instead of spending an afternoon on it.

Why Lima Breaks the Default Testcontainers Setup

Lima exposes Docker through a Unix socket at a non-standard path — typically ~/docker.sock or inside ~/.lima/ — and sets DOCKER_HOST in your shell to point to it. That is enough for the Docker CLI and most tools.

Testcontainers has two problems with this setup.

  • Maven Surefire forks a new JVM process for tests. That forked process does not automatically inherit your shell’s DOCKER_HOST. Testcontainers falls back to the default socket path (/var/run/docker.sock), finds nothing, and times out.
  • Ryuk cannot start inside Lima’s Docker. Ryuk is the resource-reaper sidecar container that Testcontainers starts to clean up after itself. It requires a privileged bind-mount of the Docker socket from inside a container, which Lima’s Docker daemon restricts by default. The result is that silent 60-second hang.

Fix both problems and everything works. Here are the five steps.

Step 1: Find Your Lima Docker Socket Path

Before configuring anything, confirm where your socket actually lives:

echo $DOCKER_HOST
# unix:///Users/you/docker.sock
# Or check directly
limactl list
# NAME STATUS SSH CPUS MEMORY DISK
# docker Running 127.0.0.1:60006 3 4GiB 70GiB

The value from $DOCKER_HOST is what you will use in the next step. If it is not set, check ~/.lima/docker/sock/docker.sock or run limactl shell docker -- docker context inspect.

Step 2: Create ~/.testcontainers.properties

Testcontainers always reads this file, regardless of how the JVM was started. It is the most reliable place to set the socket path and disable Ryuk.

# ~/.testcontainers.properties
# Point to the Lima socket
docker.host=unix:///Users/you/docker.sock
# Disable Ryuk — it cannot start inside Lima's Docker
ryuk.disabled=true

Disabling Ryuk does not mean your containers leak. It means Testcontainers will not start a separate reaper container. Containers started with @Container static are tied to the JVM process and stopped when it exits. For the vast majority of test suites this is perfectly safe.

Step 3: Pass DOCKER_HOST Through Maven Surefire

~/.testcontainers.properties handles Testcontainers’ internal config, but Maven Surefire forks a JVM that may not see your shell environment. Pass the variables explicitly in your pom.xml:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<environmentVariables>
<TESTCONTAINERS_RYUK_DISABLED>true</TESTCONTAINERS_RYUK_DISABLED>
<DOCKER_HOST>${env.DOCKER_HOST}</DOCKER_HOST>
</environmentVariables>
</configuration>
</plugin>

${env.DOCKER_HOST} reads the value from the shell that invoked Maven and forwards it to the forked process. This is belt-and-suspenders alongside ~/.testcontainers.properties, and it is what made the test pass consistently in CI as well.

Step 4: Disable Spring Boot Docker Compose in Tests

If you are using the spring-boot-docker-compose module — which starts your compose.yaml automatically on application startup — there is a conflict: both Spring Boot and Testcontainers will try to manage a Postgres instance at the same time.

The fix is a single property in your test resources:

# src/test/resources/application.properties
spring.docker.compose.enabled=false

Tests use the Testcontainers-managed Postgres. The application uses Docker Compose at runtime. Each environment gets its own database lifecycle, cleanly separated.

Step 5: Use @ServiceConnection for Zero-Config Datasource Wiring

Spring Boot 3.1 introduced @ServiceConnection, and it is the cleanest part of this whole setup. Instead of manually injecting container host/port values into application.properties, Spring Boot reads them directly from the running container and configures the datasource automatically.

@SpringBootTest
@Testcontainers
class BackendApplicationTests {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:latest");
@Test
void contextLoads() {
}
}

There is no @DynamicPropertySource, no datasource.url override, no test profile. The @ServiceConnection annotation handles all of it. This is the approach I now use as the default for any new Spring Boot project with Testcontainers.

The pom.xml Dependencies You Need

For completeness, here are the test-scoped dependencies. Spring Boot manages the Testcontainers version through its BOM — note that Spring Boot 4.x targets Testcontainers 2.x, which may not yet be widely available on Maven Central. Pin the version explicitly if you hit resolution errors:

<properties>
<testcontainers.version>1.20.4</testcontainers.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Configuration Checklist

If you are debugging a broken setup, work through this list in order:

  1. Confirm $DOCKER_HOST is set in your shell and the socket file exists.
  2. Confirm docker info works without errors from the same terminal you run Maven in.
  3. Create ~/.testcontainers.properties with docker.host and ryuk.disabled=true.
  4. Add the <environmentVariables> block to the Surefire plugin in pom.xml.
  5. Add spring.docker.compose.enabled=false to src/test/resources/application.properties.
  6. Run ./mvnw test. The first run pulls the Postgres image — subsequent runs are fast.

Does This Configuration Work in CI?

Yes, with one caveat: the CI environment needs Docker available and DOCKER_HOST set appropriately. On GitHub Actions, the standard ubuntu-latest runner has Docker pre-installed at the default socket path (/var/run/docker.sock), so Testcontainers works out of the box — no Lima-specific configuration needed. The Surefire ${env.DOCKER_HOST} expression will be empty on Linux runners and Testcontainers falls back to the default socket, which is correct.

This means the Surefire configuration is safe to commit. It works on Lima locally and on standard Docker in CI without any branching or profiles.

Key Takeaways

  • Two separate problems, two separate fixes. Ryuk failing is solved by ryuk.disabled=true. DOCKER_HOST not being inherited is solved by the Surefire <environmentVariables> block.
  • ~/.testcontainers.properties is global — it applies to every project on your machine. Good for the socket path; decide consciously whether ryuk.disabled=true should be global for you.
  • @ServiceConnection eliminates boilerplate — no more @DynamicPropertySource in every integration test class.
  • Pinning the Testcontainers version is safer than relying on the Spring Boot BOM when using early releases of Spring Boot major versions.

This post is part of the Fullstack 2026 series, covering a production-ready Spring Boot + React stack from scratch. The next post covers modern state management — how to stop using Redux for things it was never meant for.


Discover more from The Dev World – Sergio Lema

Subscribe to get the latest posts sent to your email.


Comments

Leave a comment