Repository: hibernate/hibernate-reactive
Pull request #2343 (2025-07-11 11:50:02)

[2.4] Keep image and version of the containers in Dockerfile files
Backport #2269 (PR https://github.com/hibernate/hibernate-reactive/pull/2341) to 2.4

hibernate-github-bot[bot]: Thanks for your pull request!

This pull request does not follow the contribution rules. Could you have a look?

❌ The pull request title should contain at least 2 words to describe the change properly

› This message was automatically generated.

Patch:
diff --git a/build.gradle b/build.gradle
index 7400020f..f9172203 100644
--- a/build.gradle
+++ b/build.gradle
@@ -52,6 +52,12 @@ subprojects {
         options.encoding = 'UTF-8'
     }
 
+    // Configure test tasks for all subprojects
+    tasks.withType( Test ).configureEach {
+        // Set the project root for finding Docker files - available to all modules
+        systemProperty 'hibernate.reactive.project.root', rootProject.projectDir.absolutePath
+    }
+
     if ( !gradle.ext.javaToolchainEnabled ) {
         sourceCompatibility = JavaVersion.toVersion( gradle.ext.baselineJavaVersion )
         targetCompatibility = JavaVersion.toVersion( gradle.ext.baselineJavaVersion )
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ec727128..8542b3fc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,7 +7,7 @@ jbossLoggingAnnotationVersion = "2.2.1.Final"
 jbossLoggingVersion = "3.5.0.Final"
 junitVersion = "5.11.3"
 log4jVersion = "2.20.0"
-testcontainersVersion = "1.21.0"
+testcontainersVersion = "1.21.3"
 vertxSqlClientVersion = "4.5.16"
 vertxWebVersion= "4.5.16"
 vertxWebClientVersion = "4.5.16"
diff --git a/tooling/docker/README.md b/tooling/docker/README.md
new file mode 100644
index 00000000..4de2451e
--- /dev/null
+++ b/tooling/docker/README.md
@@ -0,0 +1,6 @@
+Our test suite will only read the first FROM instruction from each Dockerfile to extract the base image and the version
+of the container to run. It will ignore everything else.
+
+The reason we have these files is that we want to automate the upgrade of the containers using dependabot.
+
+See the class `DockerImage`.
diff --git a/tooling/docker/cockroachdb.Dockerfile b/tooling/docker/cockroachdb.Dockerfile
new file mode 100644
index 00000000..a4710ad5
--- /dev/null
+++ b/tooling/docker/cockroachdb.Dockerfile
@@ -0,0 +1,3 @@
+# CockroachDB
+# See https://hub.docker.com/r/cockroachdb/cockroach
+FROM docker.io/cockroachdb/cockroach:v24.3.13
diff --git a/tooling/docker/db2.Dockerfile b/tooling/docker/db2.Dockerfile
new file mode 100644
index 00000000..1ccb3490
--- /dev/null
+++ b/tooling/docker/db2.Dockerfile
@@ -0,0 +1,3 @@
+# IBM DB2
+# See https://hub.docker.com/r/ibmcom/db2
+FROM icr.io/db2_community/db2:12.1.0.0
diff --git a/tooling/docker/maria.Dockerfile b/tooling/docker/maria.Dockerfile
new file mode 100644
index 00000000..4ccb397c
--- /dev/null
+++ b/tooling/docker/maria.Dockerfile
@@ -0,0 +1,3 @@
+# MariaDB
+# See https://hub.docker.com/_/mariadb
+FROM docker.io/mariadb:11.7.2
diff --git a/tooling/docker/mysql.Dockerfile b/tooling/docker/mysql.Dockerfile
new file mode 100644
index 00000000..966350a8
--- /dev/null
+++ b/tooling/docker/mysql.Dockerfile
@@ -0,0 +1,3 @@
+# MySQL
+# See https://hub.docker.com/_/mysql
+FROM docker.io/mysql:9.2.0
diff --git a/tooling/docker/oracle.Dockerfile b/tooling/docker/oracle.Dockerfile
new file mode 100644
index 00000000..7dfc6447
--- /dev/null
+++ b/tooling/docker/oracle.Dockerfile
@@ -0,0 +1,3 @@
+# Oracle Database Free
+# See https://hub.docker.com/r/gvenzl/oracle-free
+FROM docker.io/gvenzl/oracle-free:23-slim-faststart
diff --git a/tooling/docker/postgresql.Dockerfile b/tooling/docker/postgresql.Dockerfile
new file mode 100644
index 00000000..fb36f48e
--- /dev/null
+++ b/tooling/docker/postgresql.Dockerfile
@@ -0,0 +1,3 @@
+# PostgreSQL
+# See https://hub.docker.com/_/postgres
+FROM docker.io/postgres:17.5
diff --git a/tooling/docker/sqlserver.Dockerfile b/tooling/docker/sqlserver.Dockerfile
new file mode 100644
index 00000000..b4fb34f2
--- /dev/null
+++ b/tooling/docker/sqlserver.Dockerfile
@@ -0,0 +1,3 @@
+# Microsoft SQL Server
+# See https://hub.docker.com/_/microsoft-mssql-server
+FROM mcr.microsoft.com/mssql/server:2022-latest


Test patch:
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java
index 92cb42e6..d8bedf9a 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java
@@ -12,7 +12,7 @@
 import org.testcontainers.containers.CockroachContainer;
 import org.testcontainers.containers.Container;
 
-import static org.hibernate.reactive.containers.DockerImage.imageName;
+import static org.hibernate.reactive.containers.DockerImage.fromDockerfile;
 
 class CockroachDBDatabase extends PostgreSQLDatabase {
 
@@ -25,7 +25,7 @@ class CockroachDBDatabase extends PostgreSQLDatabase {
 	 * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located
 	 * at `$HOME/.testcontainers.properties` (create the file if it does not exist).
 	 */
-	public static final CockroachContainer cockroachDb = new CockroachContainer( imageName( "cockroachdb/cockroach", "v24.3.13" ) )
+	public static final CockroachContainer cockroachDb = new CockroachContainer( fromDockerfile( "cockroachdb" ) )
 			// Username, password and database are not supported by test container at the moment
 			// Testcontainers will use a database named 'postgres' and the 'root' user
 			.withReuse( true );
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java
index b7ac63e4..29154b2b 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java
@@ -28,7 +28,7 @@
 
 import org.testcontainers.containers.Db2Container;
 
-import static org.hibernate.reactive.containers.DockerImage.imageName;
+import static org.hibernate.reactive.containers.DockerImage.fromDockerfile;
 
 class DB2Database implements TestableDatabase {
 
@@ -87,7 +87,7 @@ class DB2Database implements TestableDatabase {
 	 * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located
 	 * at `$HOME/.testcontainers.properties` (create the file if it does not exist).
 	 */
-	static final Db2Container db2 = new Db2Container( imageName( "icr.io", "db2_community/db2", "12.1.0.0" ) )
+	static final Db2Container db2 = new Db2Container( fromDockerfile( "db2" ) )
 			.withUsername( DatabaseConfiguration.USERNAME )
 			.withPassword( DatabaseConfiguration.PASSWORD )
 			.withDatabaseName( DatabaseConfiguration.DB_NAME )
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java
index 1b2f3f6a..e8f40f34 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java
@@ -5,10 +5,17 @@
  */
 package org.hibernate.reactive.containers;
 
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
 import org.testcontainers.utility.DockerImageName;
 
+
 /**
- * A utility class with methods to generate {@link DockerImageName} for testcontainers.
+ * A utility class with methods to generate a {@link DockerImageName} for Testcontainers.
  * <p>
  * Testcontainers might not work if the image required is available in multiple different registries (for example when
  * using podman instead of docker).
@@ -17,10 +24,28 @@
  */
 public final class DockerImage {
 
-	public static final String DEFAULT_REGISTRY = "docker.io";
+	/**
+	 * The absolute path of the project root that we have set in Gradle.
+	 */
+	private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" );
+
+	/**
+	 * The path to the directory containing all the Dockerfile files
+	 */
+	private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" );
 
-	public static DockerImageName imageName(String image, String version) {
-		return imageName( DEFAULT_REGISTRY, image, version );
+	/**
+	 * Extract the image name and version from the first FROM instruction in the Dockerfile.
+	 * Note that everything else is ignored.
+	 */
+	public static DockerImageName fromDockerfile(String databaseName) {
+		try {
+			final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() );
+			return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() );
+		}
+		catch (IOException e) {
+			throw new RuntimeException( e );
+		}
 	}
 
 	public static DockerImageName imageName(String registry, String image, String version) {
@@ -28,4 +53,74 @@ public static DockerImageName imageName(String registry, String image, String ve
 				.parse( registry + "/" + image + ":" + version )
 				.asCompatibleSubstituteFor( image );
 	}
+
+	private static class ImageInformation {
+		private final String registry;
+		private final String image;
+		private final String version;
+
+		public ImageInformation(String fullImageInfo) {
+			// FullImageInfo pattern: <registry>/<image>:<version>
+			// For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13"
+			final int registryEndPos = fullImageInfo.indexOf( '/' );
+			final int imageEndPos = fullImageInfo.lastIndexOf( ':' );
+			this.registry = fullImageInfo.substring( 0, registryEndPos );
+			this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos );
+			this.version = fullImageInfo.substring( imageEndPos + 1 );
+		}
+
+		public String getRegistry() {
+			return registry;
+		}
+
+		public String getImage() {
+			return image;
+		}
+
+		public String getVersion() {
+			return version;
+		}
+
+		@Override
+		public String toString() {
+			return registry + "/" + image + ":" + version;
+		}
+	}
+
+	private static Path dockerFilePath(String database) {
+		// Get project root from system property set by Gradle, with fallback
+		return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" );
+	}
+
+	private static ImageInformation readFromInstruction(String database) throws IOException {
+		return readFromInstruction( dockerFilePath( database ) );
+	}
+
+	/**
+	 * Read a Dockerfile and extract the first FROM instruction.
+	 *
+	 * @param dockerfilePath path to the Dockerfile
+	 * @return the first FROM instruction found, or empty if none found
+	 * @throws IOException if the file cannot be read
+	 */
+	private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException {
+		if ( !Files.exists( dockerfilePath ) ) {
+			throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath );
+		}
+
+		List<String> lines = Files.readAllLines( dockerfilePath );
+		for ( String line : lines ) {
+			// Skip comments and empty lines
+			String trimmedLine = line.trim();
+			if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) {
+				continue;
+			}
+
+			if ( trimmedLine.startsWith( "FROM " ) ) {
+				return new ImageInformation( trimmedLine.substring( "FROM ".length() ) );
+			}
+		}
+
+		throw new IOException( " Missing FROM instruction in " + dockerfilePath );
+	}
 }
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java
index 8078d1b7..d0219f34 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java
@@ -28,7 +28,7 @@
 
 import org.testcontainers.containers.MSSQLServerContainer;
 
-import static org.hibernate.reactive.containers.DockerImage.imageName;
+import static org.hibernate.reactive.containers.DockerImage.fromDockerfile;
 
 /**
  * The JDBC driver syntax is:
@@ -96,7 +96,7 @@ class MSSQLServerDatabase implements TestableDatabase {
 	 * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located
 	 * at `$HOME/.testcontainers.properties` (create the file if it does not exist).
 	 */
-	public static final MSSQLServerContainer<?> mssqlserver = new MSSQLServerContainer<>( imageName( "mcr.microsoft.com", "mssql/server", "2022-latest" ) )
+	public static final MSSQLServerContainer<?> mssqlserver = new MSSQLServerContainer<>( fromDockerfile( "sqlserver" ) )
 			.acceptLicense()
 			.withPassword( PASSWORD )
 			.withReuse( true );
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java
index 6c9db8df..e704df38 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java
@@ -6,10 +6,10 @@
 package org.hibernate.reactive.containers;
 
 
-import static org.hibernate.reactive.containers.DockerImage.imageName;
-
 import org.testcontainers.containers.MariaDBContainer;
 
+import static org.hibernate.reactive.containers.DockerImage.fromDockerfile;
+
 class MariaDatabase extends MySQLDatabase {
 
 	static MariaDatabase INSTANCE = new MariaDatabase();
@@ -21,7 +21,7 @@ class MariaDatabase extends MySQLDatabase {
 	 * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located
 	 * at `$HOME/.testcontainers.properties` (create the file if it does not exist).
 	 */
-	public static final MariaDBContainer<?> maria = new MariaDBContainer<>( imageName( "mariadb", "11.7.2" ) )
+	public static final MariaDBContainer<?> maria = new MariaDBContainer<>( fromDockerfile( "maria" ) )
 			.withUsername( DatabaseConfiguration.USERNAME )
 			.withPassword( DatabaseConfiguration.PASSWORD )
 			.withDatabaseName( DatabaseConfiguration.DB_NAME )
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java
index 72c73113..142a88c5 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java
@@ -5,7 +5,7 @@
  */
 package org.hibernate.reactive.containers;
 
-import static org.hibernate.reactive.containers.DockerImage.imageName;
+import static org.hibernate.reactive.containers.DockerImage.fromDockerfile;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -87,7 +87,7 @@ class MySQLDatabase implements TestableDatabase {
 	 * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located
 	 * at `$HOME/.testcontainers.properties` (create the file if it does not exist).
 	 */
-	public static final MySQLContainer<?> mysql = new MySQLContainer<>( imageName( "mysql", "9.2.0") )
+	public static final MySQLContainer<?> mysql = new MySQLContainer<>( fromDockerfile( "mysql" ) )
 			.withUsername( DatabaseConfiguration.USERNAME )
 			.withPassword( DatabaseConfiguration.PASSWORD )
 			.withDatabaseName( DatabaseConfiguration.DB_NAME )
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java
index 9ad2f813..594ccf46 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java
@@ -29,7 +29,7 @@
 
 import org.testcontainers.containers.OracleContainer;
 
-import static org.hibernate.reactive.containers.DockerImage.imageName;
+import static org.hibernate.reactive.containers.DockerImage.fromDockerfile;
 
 /**
  * Connection string for Oracle thin should be something like:
@@ -88,9 +88,7 @@ class OracleDatabase implements TestableDatabase {
 		}
 	}
 
-	public static final OracleContainer oracle = new OracleContainer(
-				imageName( "gvenzl/oracle-free", "23-slim-faststart" )
-					.asCompatibleSubstituteFor( "gvenzl/oracle-xe" ) )
+	public static final OracleContainer oracle = new OracleContainer( fromDockerfile( "oracle" ).asCompatibleSubstituteFor( "gvenzl/oracle-xe" ) )
 			.withUsername( DatabaseConfiguration.USERNAME )
 			.withPassword( DatabaseConfiguration.PASSWORD )
 			.withDatabaseName( DatabaseConfiguration.DB_NAME )
diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java
index c19d1c06..4f31ea47 100644
--- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java
+++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java
@@ -5,8 +5,6 @@
  */
 package org.hibernate.reactive.containers;
 
-import static org.hibernate.reactive.containers.DockerImage.imageName;
-
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -30,9 +28,11 @@
 
 import org.testcontainers.containers.PostgreSQLContainer;
 
+import static org.hibernate.reactive.containers.DockerImage.fromDockerfile;
+
 class PostgreSQLDatabase implements TestableDatabase {
 
-	public static PostgreSQLDatabase INSTANCE = new PostgreSQLDatabase();
+	public static final PostgreSQLDatabase INSTANCE = new PostgreSQLDatabase();
 
 	private static Map<Class<?>, String> expectedDBTypeForClass = new HashMap<>();
 
@@ -87,7 +87,7 @@ class PostgreSQLDatabase implements TestableDatabase {
 	 * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located
 	 * at `$HOME/.testcontainers.properties` (create the file if it does not exist).
 	 */
-	public static final PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>( imageName( "postgres", "17.5" ) )
+	public static final PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>( fromDockerfile( "postgresql" ) )
 			.withUsername( DatabaseConfiguration.USERNAME )
 			.withPassword( DatabaseConfiguration.PASSWORD )
 			.withDatabaseName( DatabaseConfiguration.DB_NAME )
diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java
index 2388cb42..8f6081a8 100644
--- a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java
+++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java
@@ -52,9 +52,7 @@ public abstract class BaseReactiveIT {
 	// These properties are in DatabaseConfiguration in core
 	public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" );
 
-	public static final DockerImageName IMAGE_NAME = DockerImageName
-			.parse( "docker.io/postgres:17.5" )
-			.asCompatibleSubstituteFor( "postgres" );
+	public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" );
 
 	public static final String USERNAME = "hreact";
 	public static final String PASSWORD = "hreact";
diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java
new file mode 100644
index 00000000..b5621249
--- /dev/null
+++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java
@@ -0,0 +1,125 @@
+/* Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright: Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.reactive.it;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A utility class with methods to generate a {@link DockerImageName} for Testcontainers.
+ * <p>
+ * Testcontainers might not work if the image required is available in multiple different registries (for example when
+ * using podman instead of docker).
+ * These methods make sure to pick a registry as default.
+ * </p>
+ */
+public final class DockerImage {
+
+	/**
+	 * The absolute path of the project root that we have set in Gradle.
+	 */
+	private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" );
+
+	/**
+	 * The path to the directory containing all the Dockerfile files
+	 */
+	private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" );
+
+	/**
+	 * Extract the image name and version from the first FROM instruction in the Dockerfile.
+	 * Note that everything else is ignored.
+	 */
+	public static DockerImageName fromDockerfile(String databaseName) {
+		try {
+			final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() );
+			return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() );
+		}
+		catch (IOException e) {
+			throw new RuntimeException( e );
+		}
+	}
+
+	public static DockerImageName imageName(String registry, String image, String version) {
+		return DockerImageName
+				.parse( registry + "/" + image + ":" + version )
+				.asCompatibleSubstituteFor( image );
+	}
+
+	private static class ImageInformation {
+		private final String registry;
+		private final String image;
+		private final String version;
+
+		public ImageInformation(String fullImageInfo) {
+			// FullImageInfo pattern: <registry>/<image>:<version>
+			// For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13"
+			final int registryEndPos = fullImageInfo.indexOf( '/' );
+			final int imageEndPos = fullImageInfo.lastIndexOf( ':' );
+			this.registry = fullImageInfo.substring( 0, registryEndPos );
+			this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos );
+			this.version = fullImageInfo.substring( imageEndPos + 1 );
+		}
+
+		public String getRegistry() {
+			return registry;
+		}
+
+		public String getImage() {
+			return image;
+		}
+
+		public String getVersion() {
+			return version;
+		}
+
+		@Override
+		public String toString() {
+			return registry + "/" + image + ":" + version;
+		}
+	}
+
+	private static Path dockerFilePath(String database) {
+		// Get project root from system property set by Gradle, with fallback
+		return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" );
+	}
+
+	private static ImageInformation readFromInstruction(String database) throws IOException {
+		return readFromInstruction( dockerFilePath( database ) );
+	}
+
+	/**
+	 * Read a Dockerfile and extract the first FROM instruction.
+	 *
+	 * @param dockerfilePath path to the Dockerfile
+	 * @return the first FROM instruction found, or empty if none found
+	 * @throws IOException if the file cannot be read
+	 */
+	private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException {
+		if ( !Files.exists( dockerfilePath ) ) {
+			throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath );
+		}
+
+		List<String> lines = Files.readAllLines( dockerfilePath );
+		for ( String line : lines ) {
+			// Skip comments and empty lines
+			String trimmedLine = line.trim();
+			if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) {
+				continue;
+			}
+
+			if ( trimmedLine.startsWith( "FROM " ) ) {
+				return new ImageInformation( trimmedLine.substring( "FROM ".length() ) );
+			}
+		}
+
+		throw new IOException( " Missing FROM instruction in " + dockerfilePath );
+	}
+}
diff --git a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java
index 7debdbb2..fcefdaa8 100644
--- a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java
+++ b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java
@@ -52,9 +52,7 @@ public abstract class BaseReactiveIT {
 	// These properties are in DatabaseConfiguration in core
 	public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" );
 
-	public static final DockerImageName IMAGE_NAME = DockerImageName
-			.parse( "docker.io/postgres:17.5" )
-			.asCompatibleSubstituteFor( "postgres" );
+	public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" );
 
 	public static final String USERNAME = "hreact";
 	public static final String PASSWORD = "hreact";
diff --git a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java
new file mode 100644
index 00000000..7770da76
--- /dev/null
+++ b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java
@@ -0,0 +1,125 @@
+/* Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright: Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.reactive.it.quarkus.qe.database;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A utility class with methods to generate a {@link DockerImageName} for Testcontainers.
+ * <p>
+ * Testcontainers might not work if the image required is available in multiple different registries (for example when
+ * using podman instead of docker).
+ * These methods make sure to pick a registry as default.
+ * </p>
+ */
+public final class DockerImage {
+
+	/**
+	 * The absolute path of the project root that we have set in Gradle.
+	 */
+	private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" );
+
+	/**
+	 * The path to the directory containing all the Dockerfile files
+	 */
+	private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" );
+
+	/**
+	 * Extract the image name and version from the first FROM instruction in the Dockerfile.
+	 * Note that everything else is ignored.
+	 */
+	public static DockerImageName fromDockerfile(String databaseName) {
+		try {
+			final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() );
+			return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() );
+		}
+		catch (IOException e) {
+			throw new RuntimeException( e );
+		}
+	}
+
+	public static DockerImageName imageName(String registry, String image, String version) {
+		return DockerImageName
+				.parse( registry + "/" + image + ":" + version )
+				.asCompatibleSubstituteFor( image );
+	}
+
+	private static class ImageInformation {
+		private final String registry;
+		private final String image;
+		private final String version;
+
+		public ImageInformation(String fullImageInfo) {
+			// FullImageInfo pattern: <registry>/<image>:<version>
+			// For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13"
+			final int registryEndPos = fullImageInfo.indexOf( '/' );
+			final int imageEndPos = fullImageInfo.lastIndexOf( ':' );
+			this.registry = fullImageInfo.substring( 0, registryEndPos );
+			this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos );
+			this.version = fullImageInfo.substring( imageEndPos + 1 );
+		}
+
+		public String getRegistry() {
+			return registry;
+		}
+
+		public String getImage() {
+			return image;
+		}
+
+		public String getVersion() {
+			return version;
+		}
+
+		@Override
+		public String toString() {
+			return registry + "/" + image + ":" + version;
+		}
+	}
+
+	private static Path dockerFilePath(String database) {
+		// Get project root from system property set by Gradle, with fallback
+		return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" );
+	}
+
+	private static ImageInformation readFromInstruction(String database) throws IOException {
+		return readFromInstruction( dockerFilePath( database ) );
+	}
+
+	/**
+	 * Read a Dockerfile and extract the first FROM instruction.
+	 *
+	 * @param dockerfilePath path to the Dockerfile
+	 * @return the first FROM instruction found, or empty if none found
+	 * @throws IOException if the file cannot be read
+	 */
+	private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException {
+		if ( !Files.exists( dockerfilePath ) ) {
+			throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath );
+		}
+
+		List<String> lines = Files.readAllLines( dockerfilePath );
+		for ( String line : lines ) {
+			// Skip comments and empty lines
+			String trimmedLine = line.trim();
+			if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) {
+				continue;
+			}
+
+			if ( trimmedLine.startsWith( "FROM " ) ) {
+				return new ImageInformation( trimmedLine.substring( "FROM ".length() ) );
+			}
+		}
+
+		throw new IOException( " Missing FROM instruction in " + dockerfilePath );
+	}
+}
diff --git a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java
new file mode 100644
index 00000000..10026d02
--- /dev/null
+++ b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java
@@ -0,0 +1,125 @@
+/* Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright: Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.reactive.it.verticle;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * A utility class with methods to generate a {@link DockerImageName} for Testcontainers.
+ * <p>
+ * Testcontainers might not work if the image required is available in multiple different registries (for example when
+ * using podman instead of docker).
+ * These methods make sure to pick a registry as default.
+ * </p>
+ */
+public final class DockerImage {
+
+	/**
+	 * The absolute path of the project root that we have set in Gradle.
+	 */
+	private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" );
+
+	/**
+	 * The path to the directory containing all the Dockerfile files
+	 */
+	private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" );
+
+	/**
+	 * Extract the image name and version from the first FROM instruction in the Dockerfile.
+	 * Note that everything else is ignored.
+	 */
+	public static DockerImageName fromDockerfile(String databaseName) {
+		try {
+			final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() );
+			return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() );
+		}
+		catch (IOException e) {
+			throw new RuntimeException( e );
+		}
+	}
+
+	public static DockerImageName imageName(String registry, String image, String version) {
+		return DockerImageName
+				.parse( registry + "/" + image + ":" + version )
+				.asCompatibleSubstituteFor( image );
+	}
+
+	private static class ImageInformation {
+		private final String registry;
+		private final String image;
+		private final String version;
+
+		public ImageInformation(String fullImageInfo) {
+			// FullImageInfo pattern: <registry>/<image>:<version>
+			// For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13"
+			final int registryEndPos = fullImageInfo.indexOf( '/' );
+			final int imageEndPos = fullImageInfo.lastIndexOf( ':' );
+			this.registry = fullImageInfo.substring( 0, registryEndPos );
+			this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos );
+			this.version = fullImageInfo.substring( imageEndPos + 1 );
+		}
+
+		public String getRegistry() {
+			return registry;
+		}
+
+		public String getImage() {
+			return image;
+		}
+
+		public String getVersion() {
+			return version;
+		}
+
+		@Override
+		public String toString() {
+			return registry + "/" + image + ":" + version;
+		}
+	}
+
+	private static Path dockerFilePath(String database) {
+		// Get project root from system property set by Gradle, with fallback
+		return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" );
+	}
+
+	private static ImageInformation readFromInstruction(String database) throws IOException {
+		return readFromInstruction( dockerFilePath( database ) );
+	}
+
+	/**
+	 * Read a Dockerfile and extract the first FROM instruction.
+	 *
+	 * @param dockerfilePath path to the Dockerfile
+	 * @return the first FROM instruction found, or empty if none found
+	 * @throws IOException if the file cannot be read
+	 */
+	private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException {
+		if ( !Files.exists( dockerfilePath ) ) {
+			throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath );
+		}
+
+		List<String> lines = Files.readAllLines( dockerfilePath );
+		for ( String line : lines ) {
+			// Skip comments and empty lines
+			String trimmedLine = line.trim();
+			if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) {
+				continue;
+			}
+
+			if ( trimmedLine.startsWith( "FROM " ) ) {
+				return new ImageInformation( trimmedLine.substring( "FROM ".length() ) );
+			}
+		}
+
+		throw new IOException( " Missing FROM instruction in " + dockerfilePath );
+	}
+}
diff --git a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java
index 990fef75..f11254c6 100644
--- a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java
+++ b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java
@@ -21,6 +21,7 @@
 import io.vertx.core.Vertx;
 import io.vertx.core.VertxOptions;
 import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.utility.DockerImageName;
 
 import static java.lang.invoke.MethodHandles.lookup;
 import static org.hibernate.reactive.logging.impl.LoggerFactory.make;
@@ -36,7 +37,8 @@ public class VertxServer {
 	// These properties are in DatabaseConfiguration in core
 	public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" );
 
-	public static final String IMAGE_NAME = "postgres:17.5";
+	public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" );
+
 	public static final String USERNAME = "hreact";
 	public static final String PASSWORD = "hreact";
 	public static final String DB_NAME = "hreact";
diff --git a/tooling/jbang/CockroachDBReactiveTest.java.qute b/tooling/jbang/CockroachDBReactiveTest.java.qute
index 584d8e1d..8b87070b 100755
--- a/tooling/jbang/CockroachDBReactiveTest.java.qute
+++ b/tooling/jbang/CockroachDBReactiveTest.java.qute
@@ -10,7 +10,7 @@
 //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final}
 //DEPS org.assertj:assertj-core:3.26.3
 //DEPS junit:junit:4.13.2
-//DEPS org.testcontainers:cockroachdb:1.20.6
+//DEPS org.testcontainers:cockroachdb:1.21.3
 //DEPS org.slf4j:slf4j-simple:2.0.7
 
 //// Testcontainer needs the JDBC drivers to start the container
diff --git a/tooling/jbang/Db2ReactiveTest.java.qute b/tooling/jbang/Db2ReactiveTest.java.qute
index 2b34f8a5..8e8c6554 100755
--- a/tooling/jbang/Db2ReactiveTest.java.qute
+++ b/tooling/jbang/Db2ReactiveTest.java.qute
@@ -10,7 +10,7 @@
 //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final}
 //DEPS org.assertj:assertj-core:3.26.3
 //DEPS junit:junit:4.13.2
-//DEPS org.testcontainers:db2:1.20.6
+//DEPS org.testcontainers:db2:1.21.3
 //DEPS org.slf4j:slf4j-simple:2.0.7
 
 import jakarta.persistence.Entity;
diff --git a/tooling/jbang/MariaDBReactiveTest.java.qute b/tooling/jbang/MariaDBReactiveTest.java.qute
index f89f2129..444e0421 100755
--- a/tooling/jbang/MariaDBReactiveTest.java.qute
+++ b/tooling/jbang/MariaDBReactiveTest.java.qute
@@ -10,7 +10,7 @@
 //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final}
 //DEPS org.assertj:assertj-core:3.26.3
 //DEPS junit:junit:4.13.2
-//DEPS org.testcontainers:mariadb:1.20.6
+//DEPS org.testcontainers:mariadb:1.21.3
 //DEPS org.slf4j:slf4j-simple:2.0.7
 
 //// Testcontainer needs the JDBC drivers to start the container
diff --git a/tooling/jbang/MySQLReactiveTest.java.qute b/tooling/jbang/MySQLReactiveTest.java.qute
index 1c78cc70..4e1b6899 100755
--- a/tooling/jbang/MySQLReactiveTest.java.qute
+++ b/tooling/jbang/MySQLReactiveTest.java.qute
@@ -10,7 +10,7 @@
 //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final}
 //DEPS org.assertj:assertj-core:3.26.3
 //DEPS junit:junit:4.13.2
-//DEPS org.testcontainers:mysql:1.20.6
+//DEPS org.testcontainers:mysql:1.21.3
 //DEPS org.slf4j:slf4j-simple:2.0.7
 
 //// Testcontainer needs the JDBC drivers to start the container
diff --git a/tooling/jbang/PostgreSQLReactiveTest.java.qute b/tooling/jbang/PostgreSQLReactiveTest.java.qute
index f5a922b4..2c32c08c 100755
--- a/tooling/jbang/PostgreSQLReactiveTest.java.qute
+++ b/tooling/jbang/PostgreSQLReactiveTest.java.qute
@@ -10,7 +10,7 @@
 //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:2.4.0.Final}
 //DEPS org.assertj:assertj-core:3.26.3
 //DEPS junit:junit:4.13.2
-//DEPS org.testcontainers:postgresql:1.20.6
+//DEPS org.testcontainers:postgresql:1.21.3
 //DEPS org.slf4j:slf4j-simple:2.0.7
 //DESCRIPTION Allow authentication to PostgreSQL using SCRAM:
 //DEPS com.ongres.scram:client:2.1
diff --git a/tooling/jbang/ReactiveTest.java b/tooling/jbang/ReactiveTest.java
index 9abcda9d..8162a9ea 100755
--- a/tooling/jbang/ReactiveTest.java
+++ b/tooling/jbang/ReactiveTest.java
@@ -13,11 +13,11 @@
 //DEPS org.hibernate.reactive:hibernate-reactive-core:${hibernate-reactive.version:2.4.0.Final}
 //DEPS org.assertj:assertj-core:3.26.3
 //DEPS junit:junit:4.13.2
-//DEPS org.testcontainers:postgresql:1.20.6
-//DEPS org.testcontainers:mysql:1.20.6
-//DEPS org.testcontainers:db2:1.20.6
-//DEPS org.testcontainers:mariadb:1.20.6
-//DEPS org.testcontainers:cockroachdb:1.20.6
+//DEPS org.testcontainers:postgresql:1.21.3
+//DEPS org.testcontainers:mysql:1.21.3
+//DEPS org.testcontainers:db2:1.21.3
+//DEPS org.testcontainers:mariadb:1.21.3
+//DEPS org.testcontainers:cockroachdb:1.21.3
 //
 //// Testcontainer needs the JDBC drivers to start the containers
 //// Hibernate Reactive doesn't use them