From 4e553f92cf7276bb081e25a5598d029da45cc986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20G=C3=A4ngel?= Date: Thu, 26 Feb 2026 15:24:13 +0100 Subject: [PATCH] Add configurable log driver for containers --- .../containers/GenericContainer.java | 16 +++ .../utility/TestcontainersConfiguration.java | 7 ++ .../ContainerLogDriverConfigurationTest.java | 117 ++++++++++++++++++ .../containers/GenericContainerTest.java | 23 ++++ .../TestcontainersConfigurationTest.java | 29 +++++ docs/features/configuration.md | 18 ++- 6 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/org/testcontainers/containers/ContainerLogDriverConfigurationTest.java diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 0fe944433ae..716102f5683 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -12,6 +12,7 @@ import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.HostConfig; import com.github.dockerjava.api.model.Link; +import com.github.dockerjava.api.model.LogConfig; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.api.model.Volume; @@ -609,6 +610,21 @@ private HostConfig buildHostConfig(HostConfig config) { if (tmpFsMapping != null) { config.withTmpFs(tmpFsMapping); } + TestcontainersConfiguration + .getInstance() + .getContainerLogDriver() + .ifPresent(driver -> { + LogConfig.LoggingType type = LogConfig.LoggingType.fromValue(driver); + if (type != null) { + config.withLogConfig(new LogConfig(type)); + } else { + logger() + .warn( + "Container log driver '{}' is not recognized by the docker-java client library and cannot be applied, ignoring.", + driver + ); + } + }); return config; } diff --git a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java index 5c80b82ba9c..d48093faa49 100644 --- a/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java +++ b/core/src/main/java/org/testcontainers/utility/TestcontainersConfiguration.java @@ -220,6 +220,13 @@ public String getImagePullPolicy() { return getEnvVarOrProperty("pull.policy", null); } + public Optional getContainerLogDriver() { + return Optional + .ofNullable(getEnvVarOrProperty("container.log.driver", null)) + .map(String::trim) + .filter(s -> !s.isEmpty()); + } + public Integer getClientPingTimeout() { return Integer.parseInt(getEnvVarOrProperty("client.ping.timeout", "10")); } diff --git a/core/src/test/java/org/testcontainers/containers/ContainerLogDriverConfigurationTest.java b/core/src/test/java/org/testcontainers/containers/ContainerLogDriverConfigurationTest.java new file mode 100644 index 00000000000..329bcceffe6 --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/ContainerLogDriverConfigurationTest.java @@ -0,0 +1,117 @@ +package org.testcontainers.containers; + +import com.github.dockerjava.api.model.LogConfig; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.TestImages; +import org.testcontainers.utility.MockTestcontainersConfigurationExtension; +import org.testcontainers.utility.TestcontainersConfiguration; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +@ExtendWith(MockTestcontainersConfigurationExtension.class) +class ContainerLogDriverConfigurationTest { + + private static String daemonDefault; + + @BeforeAll + static void fetchDaemonDefault() { + daemonDefault = DockerClientFactory.instance().client().infoCmd().exec().getLoggingDriver(); + } + + @Test + void shouldApplyConfiguredLogDriverToContainer() { + LogConfig.LoggingType overrideDriver = daemonDefault.equals(LogConfig.LoggingType.NONE.getType()) + ? LogConfig.LoggingType.JSON_FILE + : LogConfig.LoggingType.NONE; + + Mockito + .doReturn(Optional.of(overrideDriver.getType())) + .when(TestcontainersConfiguration.getInstance()) + .getContainerLogDriver(); + + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withCommand("tail", "-f", "/dev/null") + ) { + container.start(); + + assertThat(container.getContainerInfo().getHostConfig().getLogConfig().getType().getType()) + .as("container should use the configured log driver instead of the daemon default (%s)", daemonDefault) + .isEqualTo(overrideDriver.getType()); + } + } + + @Test + void shouldNotOverrideLogDriverWhenNotConfigured() { + Mockito.doReturn(Optional.empty()).when(TestcontainersConfiguration.getInstance()).getContainerLogDriver(); + + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withCommand("tail", "-f", "/dev/null") + ) { + container.start(); + + assertThat(container.getContainerInfo().getHostConfig().getLogConfig().getType().getType()) + .as("container should use the daemon default log driver when none is configured") + .isEqualTo(daemonDefault); + } + } + + @Test + void shouldAllowPerContainerOverrideOfGlobalLogDriver() { + // Set global config to the non-default driver + LogConfig.LoggingType globalDriver = daemonDefault.equals(LogConfig.LoggingType.NONE.getType()) + ? LogConfig.LoggingType.JSON_FILE + : LogConfig.LoggingType.NONE; + Mockito + .doReturn(Optional.of(globalDriver.getType())) + .when(TestcontainersConfiguration.getInstance()) + .getContainerLogDriver(); + + // Pick a different driver to override with at the per-container level + LogConfig.LoggingType perContainerDriver = globalDriver == LogConfig.LoggingType.NONE + ? LogConfig.LoggingType.JSON_FILE + : LogConfig.LoggingType.NONE; + + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withCreateContainerCmdModifier(cmd -> { + cmd.getHostConfig().withLogConfig(new LogConfig(perContainerDriver)); + }) + .withCommand("tail", "-f", "/dev/null") + ) { + container.start(); + + assertThat(container.getContainerInfo().getHostConfig().getLogConfig().getType().getType()) + .as("per-container modifier should override the global container.log.driver setting") + .isNotEqualTo(globalDriver.getType()) + .isEqualTo(perContainerDriver.getType()); + } + } + + @Test + void shouldWarnAndIgnoreUnsupportedLogDriver() { + Mockito + .doReturn(Optional.of("invalid-driver-xyz")) + .when(TestcontainersConfiguration.getInstance()) + .getContainerLogDriver(); + + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withCommand("tail", "-f", "/dev/null") + ) { + assertThatNoException().isThrownBy(container::start); + + assertThat(container.getContainerInfo().getHostConfig().getLogConfig().getType().getType()) + .as("container should fall back to the daemon default log driver when an invalid driver is configured") + .isEqualTo(daemonDefault); + } + } +} diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java index 37aac1b0af9..61fb1e1e31b 100644 --- a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java @@ -8,6 +8,7 @@ import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.Info; +import com.github.dockerjava.api.model.LogConfig; import com.github.dockerjava.api.model.Ports; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; @@ -232,6 +233,28 @@ void testArchitectureCheck() { } } + @Test + void shouldApplyLogDriverFromHostConfig() { + String daemonDefault = DockerClientFactory.instance().client().infoCmd().exec().getLoggingDriver(); + LogConfig.LoggingType overrideDriver = daemonDefault.equals(LogConfig.LoggingType.NONE.getType()) + ? LogConfig.LoggingType.JSON_FILE + : LogConfig.LoggingType.NONE; + + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withCreateContainerCmdModifier(cmd -> { + cmd.getHostConfig().withLogConfig(new LogConfig(overrideDriver)); + }) + .withCommand("tail", "-f", "/dev/null") + ) { + container.start(); + + assertThat(container.getContainerInfo().getHostConfig().getLogConfig().getType().getType()) + .as("container should use the configured log driver instead of the daemon default (%s)", daemonDefault) + .isEqualTo(overrideDriver.getType()); + } + } + @Test void shouldReturnTheProvidedImage() { GenericContainer container = new GenericContainer(TestImages.REDIS_IMAGE); diff --git a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java index 4e0520af5ba..e85a499bff7 100644 --- a/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java +++ b/core/src/test/java/org/testcontainers/utility/TestcontainersConfigurationTest.java @@ -217,6 +217,35 @@ void shouldReadReuseFromEnvironment() { assertThat(newConfig().environmentSupportsReuse()).as("reuse enabled via env var").isTrue(); } + @Test + void shouldReturnEmptyOptionalWhenContainerLogDriverNotSet() { + assertThat(newConfig().getContainerLogDriver()).as("no container log driver override by default").isEmpty(); + } + + @Test + void shouldReadContainerLogDriverFromClasspathProperties() { + classpathProperties.setProperty("container.log.driver", "json-file"); + assertThat(newConfig().getContainerLogDriver()) + .as("container log driver can be set via classpath properties") + .hasValue("json-file"); + } + + @Test + void shouldReadContainerLogDriverFromUserProperties() { + userProperties.setProperty("container.log.driver", "json-file"); + assertThat(newConfig().getContainerLogDriver()) + .as("container log driver can be set via user properties") + .hasValue("json-file"); + } + + @Test + void shouldReadContainerLogDriverFromEnvironmentVariable() { + environment.put("TESTCONTAINERS_CONTAINER_LOG_DRIVER", "json-file"); + assertThat(newConfig().getContainerLogDriver()) + .as("container log driver can be set via environment variable") + .hasValue("json-file"); + } + @Test void shouldTrimImageNames() { userProperties.setProperty("ryuk.container.image", " testcontainers/ryuk:0.3.2 "); diff --git a/docs/features/configuration.md b/docs/features/configuration.md index c13c0a659e3..bf8616cdffa 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -80,7 +80,7 @@ Some companies disallow the usage of Docker Hub, but you can override `*.image` > In some environments ryuk must be started in privileged mode to work properly (--privileged flag) ### Disabling Ryuk -Ryuk must be started as a privileged container. +Ryuk must be started as a privileged container. If your environment already implements automatic cleanup of containers after the execution, but does not allow starting privileged containers, you can turn off the Ryuk container by setting `TESTCONTAINERS_RYUK_DISABLED` **environment variable** to `true`. @@ -88,6 +88,22 @@ but does not allow starting privileged containers, you can turn off the Ryuk con !!!tip Note that Testcontainers will continue doing the cleanup at JVM's shutdown, unless you `kill -9` your JVM process. +## Customizing container log driver + +> **container.log.driver** (env var: `TESTCONTAINERS_CONTAINER_LOG_DRIVER`) +> Sets the default Docker log driver for all containers started by Testcontainers (e.g. `json-file`, `syslog`). +> Useful when the Docker daemon is configured with a default log driver (such as `none`) that is incompatible with log-based wait strategies. +> This default can still be overridden on a per-container basis using `withCreateContainerCmdModifier`, since modifiers run after the host config is built. +> Defaults to the Docker daemon's configured log driver if not set. +> The value must be recognized by the docker-java client library (e.g. `json-file`, `none`, `syslog`, `journald`). + +!!! warning + This setting applies to **all** containers started by Testcontainers, including the Ryuk resource reaper. + Setting this to `none` will break Ryuk's log-based wait strategy. + +!!! warning + Only log driver names recognized by the docker-java client library are supported. Unrecognized values are silently ignored with a warning. + ## Customizing image pull behaviour > **pull.timeout = 120**