diff --git a/docs/test_framework_integration/junit_5.md b/docs/test_framework_integration/junit_5.md
index aed2d515af9..b2115c9ef2c 100644
--- a/docs/test_framework_integration/junit_5.md
+++ b/docs/test_framework_integration/junit_5.md
@@ -50,6 +50,22 @@ This is because nested test classes have to be defined non-static and can't ther
[Shared Container](../../modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/MixedLifecycleTests.java) lines:18-23,32-33,35-36
+## Shared containers across test classes
+
+When multiple test classes extend a common base class, using `@Testcontainers` causes containers
+to be stopped and restarted for each test class. To share containers for the entire JVM session
+instead, use `@SharedContainers`.
+
+`@SharedContainers` stores `static` `@Container` fields in a JVM-wide store so they are started
+only once and stopped automatically at the end of the test suite.
+
+
+[Shared Across Classes](../../modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/SharedContainersBaseTest.java)
+
+
+**Note:** Do not combine `@SharedContainers` with `@Testcontainers` on the same class hierarchy,
+as this may produce unexpected lifecycle behaviour.
+
## Singleton containers
Note that the [singleton container pattern](manual_lifecycle_control.md#singleton-containers) is also an option when
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/SharedContainers.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/SharedContainers.java
new file mode 100644
index 00000000000..f18feb903ca
--- /dev/null
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/SharedContainers.java
@@ -0,0 +1,80 @@
+package org.testcontainers.junit.jupiter;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * {@code @SharedContainers} is a JUnit Jupiter extension to activate automatic startup of
+ * containers that are shared across multiple test classes.
+ *
+ *
Unlike {@link Testcontainers}, which manages container lifecycle per test class,
+ * {@code @SharedContainers} uses the root {@link org.junit.jupiter.api.extension.ExtensionContext}
+ * store so that containers declared as {@code static} fields annotated with {@link Container} are
+ * started only once for the entire test suite (JVM session) and stopped automatically when the
+ * JVM exits (via Ryuk or JVM shutdown).
+ *
+ *
This is the recommended approach when multiple test classes extend a common base class and
+ * need to share the same container instances. Using {@link Testcontainers} in this scenario causes
+ * containers to be restarted for each test class.
+ *
+ *
Containers declared as instance fields will still be started and stopped for every test
+ * method, just like with {@link Testcontainers}.
+ *
+ *
The annotation {@code @SharedContainers} can be used on a superclass in the test hierarchy.
+ * All subclasses will automatically inherit support for the extension.
+ *
+ *
Example:
+ *
+ *
+ * @SharedContainers
+ * abstract class AbstractIntegrationTest {
+ *
+ * // started once for the entire test suite, shared across all subclasses
+ * @Container
+ * static final MySQLContainer<?> MY_SQL = new MySQLContainer<>("mysql:8");
+ * }
+ *
+ * class UserServiceTest extends AbstractIntegrationTest {
+ * @Test
+ * void test() {
+ * assertTrue(MY_SQL.isRunning());
+ * }
+ * }
+ *
+ * class OrderServiceTest extends AbstractIntegrationTest {
+ * @Test
+ * void test() {
+ * // same MY_SQL instance as UserServiceTest
+ * assertTrue(MY_SQL.isRunning());
+ * }
+ * }
+ *
+ *
+ * @see Container
+ * @see Testcontainers
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(SharedContainersExtension.class)
+@Inherited
+public @interface SharedContainers {
+ /**
+ * Whether tests should be disabled (rather than failing) when Docker is not available.
+ * Defaults to {@code false}.
+ *
+ * @return if the tests should be disabled when Docker is not available
+ */
+ boolean disabledWithoutDocker() default false;
+
+ /**
+ * Whether containers should start in parallel. Defaults to {@code false}.
+ *
+ * @return if the containers should start in parallel
+ */
+ boolean parallel() default false;
+}
diff --git a/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/SharedContainersExtension.java b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/SharedContainersExtension.java
new file mode 100644
index 00000000000..c68af0bbefd
--- /dev/null
+++ b/modules/junit-jupiter/src/main/java/org/testcontainers/junit/jupiter/SharedContainersExtension.java
@@ -0,0 +1,287 @@
+package org.testcontainers.junit.jupiter;
+
+import lombok.Getter;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionConfigurationException;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.ExtensionContext.Store;
+import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+import org.junit.platform.commons.support.ModifierSupport;
+import org.junit.platform.commons.support.ReflectionSupport;
+import org.testcontainers.lifecycle.Startable;
+import org.testcontainers.lifecycle.Startables;
+import org.testcontainers.lifecycle.TestDescription;
+import org.testcontainers.lifecycle.TestLifecycleAware;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * JUnit Jupiter Extension that backs the {@link SharedContainers} annotation.
+ *
+ *
Unlike {@link TestcontainersExtension}, which stores containers in a class-scoped
+ * {@link ExtensionContext.Store} (causing restart per test class), this extension stores
+ * {@code static} {@link Container}-annotated fields in the root store so that
+ * they are started at most once per JVM session and live until the JVM exits.
+ */
+public class SharedContainersExtension
+ implements BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback, ExecutionCondition {
+
+ private static final Namespace NAMESPACE = Namespace.create(SharedContainersExtension.class);
+
+ private static final String SHARED_LIFECYCLE_AWARE_CONTAINERS = "sharedLifecycleAwareContainers";
+
+ private static final String LOCAL_LIFECYCLE_AWARE_CONTAINERS = "localLifecycleAwareContainers";
+
+ private final DockerAvailableDetector dockerDetector = new DockerAvailableDetector();
+
+ @Override
+ public void beforeAll(ExtensionContext context) {
+ Class> testClass = context
+ .getTestClass()
+ .orElseThrow(() -> new ExtensionConfigurationException("SharedContainersExtension is only supported for classes."));
+
+ // Use ROOT store so containers survive across test class boundaries.
+ Store rootStore = context.getRoot().getStore(NAMESPACE);
+ List sharedContainersStoreAdapters = findSharedContainers(testClass);
+
+ startContainersInStore(sharedContainersStoreAdapters, rootStore, context);
+
+ List lifecycleAwareContainers = sharedContainersStoreAdapters
+ .stream()
+ .filter(this::isTestLifecycleAware)
+ .map(adapter -> (TestLifecycleAware) adapter.container)
+ .collect(Collectors.toList());
+
+ // Store in class-level store so afterAll can retrieve and signal afterTest()
+ context.getStore(NAMESPACE).put(SHARED_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers);
+ signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
+ }
+
+ @Override
+ public void afterAll(ExtensionContext context) {
+ signalAfterTestToContainersFor(SHARED_LIFECYCLE_AWARE_CONTAINERS, context);
+ }
+
+ @Override
+ public void beforeEach(ExtensionContext context) {
+ Store store = context.getStore(NAMESPACE);
+
+ List restartContainers = collectParentTestInstances(context)
+ .parallelStream()
+ .flatMap(this::findRestartContainers)
+ .collect(Collectors.toList());
+
+ List lifecycleAwareContainers = startContainersAndCollectLifecycleAware(
+ restartContainers,
+ store,
+ context
+ );
+
+ store.put(LOCAL_LIFECYCLE_AWARE_CONTAINERS, lifecycleAwareContainers);
+ signalBeforeTestToContainers(lifecycleAwareContainers, testDescriptionFrom(context));
+ }
+
+ @Override
+ public void afterEach(ExtensionContext context) {
+ signalAfterTestToContainersFor(LOCAL_LIFECYCLE_AWARE_CONTAINERS, context);
+ }
+
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
+ return findSharedContainersAnnotation(context)
+ .map(this::evaluate)
+ .orElseThrow(() -> new ExtensionConfigurationException("@SharedContainers not found"));
+ }
+
+ private ConditionEvaluationResult evaluate(SharedContainers annotation) {
+ if (annotation.disabledWithoutDocker()) {
+ if (isDockerAvailable()) {
+ return ConditionEvaluationResult.enabled("Docker is available");
+ }
+ return ConditionEvaluationResult.disabled("disabledWithoutDocker is true and Docker is not available");
+ }
+ return ConditionEvaluationResult.enabled("disabledWithoutDocker is false");
+ }
+
+ boolean isDockerAvailable() {
+ return this.dockerDetector.isDockerAvailable();
+ }
+
+ private Optional findSharedContainersAnnotation(ExtensionContext context) {
+ Optional current = Optional.of(context);
+ while (current.isPresent()) {
+ Optional annotation = AnnotationSupport.findAnnotation(
+ current.get().getRequiredTestClass(),
+ SharedContainers.class
+ );
+ if (annotation.isPresent()) {
+ return annotation;
+ }
+ current = current.get().getParent();
+ }
+ return Optional.empty();
+ }
+
+ private boolean isParallelExecutionEnabled(ExtensionContext context) {
+ return findSharedContainersAnnotation(context).map(SharedContainers::parallel).orElse(false);
+ }
+
+ private void startContainersInStore(List adapters, Store store, ExtensionContext context) {
+ if (adapters.isEmpty()) {
+ return;
+ }
+ if (isParallelExecutionEnabled(context)) {
+ Stream startables = adapters.stream().map(adapter -> {
+ store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter);
+ return adapter.container;
+ });
+ Startables.deepStart(startables).join();
+ } else {
+ adapters.forEach(adapter -> store.getOrComputeIfAbsent(adapter.getKey(), k -> adapter.start()));
+ }
+ }
+
+ private List startContainersAndCollectLifecycleAware(
+ List adapters,
+ Store store,
+ ExtensionContext context
+ ) {
+ startContainersInStore(adapters, store, context);
+ return adapters
+ .stream()
+ .filter(this::isTestLifecycleAware)
+ .map(adapter -> (TestLifecycleAware) adapter.container)
+ .collect(Collectors.toList());
+ }
+
+ private List findSharedContainers(Class> testClass) {
+ return ReflectionSupport
+ .findFields(testClass, isSharedContainer(), HierarchyTraversalMode.TOP_DOWN)
+ .stream()
+ .map(f -> getContainerInstance(null, f))
+ .collect(Collectors.toList());
+ }
+
+ private Predicate isSharedContainer() {
+ return isContainer().and(ModifierSupport::isStatic);
+ }
+
+ private Stream findRestartContainers(Object testInstance) {
+ return ReflectionSupport
+ .findFields(testInstance.getClass(), isRestartContainer(), HierarchyTraversalMode.TOP_DOWN)
+ .stream()
+ .map(f -> getContainerInstance(testInstance, f));
+ }
+
+ private Predicate isRestartContainer() {
+ return isContainer().and(ModifierSupport::isNotStatic);
+ }
+
+ private static Predicate isContainer() {
+ return field -> {
+ boolean isAnnotatedWithContainer = AnnotationSupport.isAnnotated(field, Container.class);
+ if (isAnnotatedWithContainer) {
+ boolean isStartable = Startable.class.isAssignableFrom(field.getType());
+ if (!isStartable) {
+ throw new ExtensionConfigurationException(
+ String.format("FieldName: %s does not implement Startable", field.getName())
+ );
+ }
+ return true;
+ }
+ return false;
+ };
+ }
+
+ private static StoreAdapter getContainerInstance(Object testInstance, Field field) {
+ try {
+ field.setAccessible(true);
+ Startable containerInstance = (Startable) field.get(testInstance);
+ if (containerInstance == null) {
+ throw new ExtensionConfigurationException("Container " + field.getName() + " needs to be initialized");
+ }
+ return new StoreAdapter(field.getDeclaringClass(), field.getName(), containerInstance);
+ } catch (IllegalAccessException e) {
+ throw new ExtensionConfigurationException("Can not access container defined in field " + field.getName());
+ }
+ }
+
+ private boolean isTestLifecycleAware(StoreAdapter adapter) {
+ return adapter.container instanceof TestLifecycleAware;
+ }
+
+ private void signalBeforeTestToContainers(List containers, TestDescription description) {
+ containers.forEach(c -> c.beforeTest(description));
+ }
+
+ private void signalAfterTestToContainersFor(String storeKey, ExtensionContext context) {
+ List lifecycleAwareContainers = (List) context
+ .getStore(NAMESPACE)
+ .get(storeKey);
+ if (lifecycleAwareContainers != null) {
+ TestDescription description = testDescriptionFrom(context);
+ Optional throwable = context.getExecutionException();
+ lifecycleAwareContainers.forEach(container -> container.afterTest(description, throwable));
+ }
+ }
+
+ private Set