Testcontainers PostgreSQL module supports multiple PostgreSQL distributions with specialized extensions including PostGIS (geographic information systems), TimescaleDB (time-series data), and PgVector (vector similarity search). Each variant has its own provider for automatic container creation via JDBC URL patterns and can also be used directly with PostgreSQLContainer.
PostGIS adds geographic information system (GIS) capabilities to PostgreSQL, enabling storage and querying of geographic objects.
/**
* Provider for creating PostGIS containers via JDBC URL patterns
* PostGIS extends PostgreSQL with geographic object support
*/
public class PostgisContainerProvider extends JdbcDatabaseContainerProvider {
// URL parameter names
public static final String USER_PARAM = "user";
public static final String PASSWORD_PARAM = "password";
/**
* Check if this provider supports the given database type
* @param databaseType Database type string
* @return true if databaseType is "postgis", false otherwise
*/
public boolean supports(String databaseType);
/**
* Create a new PostGIS container with default image (postgis/postgis:12-3.0)
* @return New JdbcDatabaseContainer with PostGIS
* @throws ContainerLaunchException if container fails to start
*/
public JdbcDatabaseContainer newInstance();
/**
* Create a new PostGIS container with specific version tag
* @param tag PostGIS version tag (e.g., "16-3.4", "15-3.3-alpine")
* @return New JdbcDatabaseContainer with PostGIS
* @throws IllegalArgumentException if tag is invalid
* @throws ContainerLaunchException if container fails to start
*/
public JdbcDatabaseContainer newInstance(String tag);
/**
* Create a new PostGIS container from a parsed JDBC connection URL
* Extracts database name, user, password, and other parameters from URL
* @param connectionUrl Parsed connection URL object
* @return New JdbcDatabaseContainer configured from URL
* @throws IllegalArgumentException if connectionUrl is invalid
*/
public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl);
}JDBC URL Format:
jdbc:tc:postgis:<version>://<host>/<database>?user=<user>&password=<password>Usage Examples:
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
// Using PostgreSQLContainer with PostGIS image
public class PostGISTest {
public void testWithPostGIS() throws Exception {
DockerImageName postgisImage = DockerImageName
.parse("postgis/postgis:16-3.4")
.asCompatibleSubstituteFor("postgres");
try (PostgreSQLContainer<?> postgis = new PostgreSQLContainer<>(postgisImage)) {
postgis.start();
String jdbcUrl = postgis.getJdbcUrl();
try (Connection conn = DriverManager.getConnection(
jdbcUrl,
postgis.getUsername(),
postgis.getPassword())) {
// Enable PostGIS extension
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE EXTENSION IF NOT EXISTS postgis");
// Query PostGIS version
try (ResultSet rs = stmt.executeQuery("SELECT PostGIS_Version()")) {
rs.next();
String version = rs.getString(1);
// PostGIS version info
}
// Create table with geography column
stmt.execute(
"CREATE TABLE locations (" +
"id SERIAL PRIMARY KEY, " +
"name VARCHAR(100), " +
"coordinates GEOGRAPHY(POINT, 4326))"
);
// Insert geographic data
stmt.execute(
"INSERT INTO locations (name, coordinates) " +
"VALUES ('New York', ST_GeogFromText('POINT(-74.0060 40.7128)'))"
);
// Spatial query
try (ResultSet rs = stmt.executeQuery(
"SELECT name, ST_AsText(coordinates) " +
"FROM locations")) {
while (rs.next()) {
String name = rs.getString(1);
String coords = rs.getString(2);
// name: "New York", coords: "POINT(-74.006 40.7128)"
}
}
}
}
}
}
}PostGIS with JDBC URL Pattern:
// Automatic container management via JDBC URL
public void testPostGISWithJdbcUrl() throws Exception {
String jdbcUrl = "jdbc:tc:postgis:16-3.4:///testdb?user=test&password=test";
try (Connection conn = DriverManager.getConnection(jdbcUrl)) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE EXTENSION IF NOT EXISTS postgis");
// Use PostGIS functions
try (ResultSet rs = stmt.executeQuery(
"SELECT ST_Distance(" +
"ST_GeogFromText('POINT(-74.0060 40.7128)'), " + // New York
"ST_GeogFromText('POINT(-118.2437 34.0522)')" + // Los Angeles
")")) {
rs.next();
double distance = rs.getDouble(1);
// Distance in meters between NYC and LA
}
}
}
}Available PostGIS Versions:
postgis/postgis:16-3.4 - PostgreSQL 16 with PostGIS 3.4postgis/postgis:16-3.4-alpine - Alpine variant (smaller image)postgis/postgis:15-3.3 - PostgreSQL 15 with PostGIS 3.3postgis/postgis:14-3.2 - PostgreSQL 14 with PostGIS 3.2postgis/postgis:12-3.0 - PostgreSQL 12 with PostGIS 3.0 (default)TimescaleDB extends PostgreSQL with time-series database capabilities, optimized for handling time-series data at scale.
/**
* Provider for creating TimescaleDB containers via JDBC URL patterns
* TimescaleDB extends PostgreSQL with time-series capabilities
*/
public class TimescaleDBContainerProvider extends JdbcDatabaseContainerProvider {
// URL parameter names
public static final String USER_PARAM = "user";
public static final String PASSWORD_PARAM = "password";
/**
* Check if this provider supports the given database type
* @param databaseType Database type string
* @return true if databaseType is "timescaledb", false otherwise
*/
public boolean supports(String databaseType);
/**
* Create a new TimescaleDB container with default image (timescale/timescaledb:2.1.0-pg11)
* @return New JdbcDatabaseContainer with TimescaleDB
* @throws ContainerLaunchException if container fails to start
*/
public JdbcDatabaseContainer newInstance();
/**
* Create a new TimescaleDB container with specific version tag
* @param tag TimescaleDB version tag (e.g., "2.14.2-pg16", "2.13.0-pg15")
* @return New JdbcDatabaseContainer with TimescaleDB
* @throws IllegalArgumentException if tag is invalid
* @throws ContainerLaunchException if container fails to start
*/
public JdbcDatabaseContainer newInstance(String tag);
/**
* Create a new TimescaleDB container from a parsed JDBC connection URL
* Extracts database name, user, password, and other parameters from URL
* @param connectionUrl Parsed connection URL object
* @return New JdbcDatabaseContainer configured from URL
* @throws IllegalArgumentException if connectionUrl is invalid
*/
public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl);
}JDBC URL Format:
jdbc:tc:timescaledb:<version>://<host>/<database>?user=<user>&password=<password>Usage Examples:
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.Timestamp;
// Using PostgreSQLContainer with TimescaleDB image
public class TimescaleDBTest {
public void testWithTimescaleDB() throws Exception {
DockerImageName timescaleImage = DockerImageName
.parse("timescale/timescaledb:2.14.2-pg16")
.asCompatibleSubstituteFor("postgres");
try (PostgreSQLContainer<?> timescale = new PostgreSQLContainer<>(timescaleImage)) {
timescale.start();
String jdbcUrl = timescale.getJdbcUrl();
try (Connection conn = DriverManager.getConnection(
jdbcUrl,
timescale.getUsername(),
timescale.getPassword())) {
try (Statement stmt = conn.createStatement()) {
// Enable TimescaleDB extension
stmt.execute("CREATE EXTENSION IF NOT EXISTS timescaledb");
// Create regular table
stmt.execute(
"CREATE TABLE sensor_data (" +
"time TIMESTAMPTZ NOT NULL, " +
"sensor_id INTEGER NOT NULL, " +
"temperature DOUBLE PRECISION, " +
"humidity DOUBLE PRECISION)"
);
// Convert to hypertable (time-series optimized)
stmt.execute(
"SELECT create_hypertable('sensor_data', 'time')"
);
// Insert time-series data
String insertSql =
"INSERT INTO sensor_data (time, sensor_id, temperature, humidity) " +
"VALUES (?, ?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
long now = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
pstmt.setTimestamp(1, new Timestamp(now - (i * 60000))); // Every minute
pstmt.setInt(2, 1);
pstmt.setDouble(3, 20.0 + Math.random() * 10);
pstmt.setDouble(4, 40.0 + Math.random() * 20);
pstmt.addBatch();
}
pstmt.executeBatch();
}
// Time-series query with time_bucket
String querySql =
"SELECT time_bucket('5 minutes', time) AS bucket, " +
"AVG(temperature) as avg_temp, " +
"MAX(temperature) as max_temp " +
"FROM sensor_data " +
"WHERE sensor_id = 1 " +
"GROUP BY bucket " +
"ORDER BY bucket DESC";
try (ResultSet rs = stmt.executeQuery(querySql)) {
while (rs.next()) {
Timestamp bucket = rs.getTimestamp("bucket");
double avgTemp = rs.getDouble("avg_temp");
double maxTemp = rs.getDouble("max_temp");
// Time-bucketed aggregates
}
}
}
}
}
}
}TimescaleDB with JDBC URL Pattern:
// Automatic container management via JDBC URL
public void testTimescaleDBWithJdbcUrl() throws Exception {
String jdbcUrl = "jdbc:tc:timescaledb:2.14.2-pg16:///testdb";
try (Connection conn = DriverManager.getConnection(jdbcUrl)) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE EXTENSION IF NOT EXISTS timescaledb");
// Create and configure hypertable
stmt.execute(
"CREATE TABLE metrics (" +
"time TIMESTAMPTZ NOT NULL, " +
"value DOUBLE PRECISION)"
);
stmt.execute("SELECT create_hypertable('metrics', 'time')");
// Use time-series features
// ...
}
}
}Available TimescaleDB Versions:
timescale/timescaledb:2.14.2-pg16 - TimescaleDB 2.14.2 on PostgreSQL 16timescale/timescaledb:2.13.1-pg15 - TimescaleDB 2.13.1 on PostgreSQL 15timescale/timescaledb:2.11.2-pg14 - TimescaleDB 2.11.2 on PostgreSQL 14timescale/timescaledb:2.1.0-pg11 - TimescaleDB 2.1.0 on PostgreSQL 11 (default)PgVector adds vector similarity search capabilities to PostgreSQL, enabling efficient storage and querying of embeddings for machine learning and AI applications.
/**
* Provider for creating PgVector containers via JDBC URL patterns
* PgVector extends PostgreSQL with vector similarity search
*/
public class PgVectorContainerProvider extends JdbcDatabaseContainerProvider {
// URL parameter names
public static final String USER_PARAM = "user";
public static final String PASSWORD_PARAM = "password";
/**
* Check if this provider supports the given database type
* @param databaseType Database type string
* @return true if databaseType is "pgvector", false otherwise
*/
public boolean supports(String databaseType);
/**
* Create a new PgVector container with default image (pgvector/pgvector:pg16)
* @return New JdbcDatabaseContainer with PgVector
* @throws ContainerLaunchException if container fails to start
*/
public JdbcDatabaseContainer newInstance();
/**
* Create a new PgVector container with specific version tag
* @param tag PgVector version tag (e.g., "pg16", "pg15", "pg14")
* @return New JdbcDatabaseContainer with PgVector
* @throws IllegalArgumentException if tag is invalid
* @throws ContainerLaunchException if container fails to start
*/
public JdbcDatabaseContainer newInstance(String tag);
/**
* Create a new PgVector container from a parsed JDBC connection URL
* Extracts database name, user, password, and other parameters from URL
* @param connectionUrl Parsed connection URL object
* @return New JdbcDatabaseContainer configured from URL
* @throws IllegalArgumentException if connectionUrl is invalid
*/
public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl);
}JDBC URL Format:
jdbc:tc:pgvector://<host>/<database>?user=<user>&password=<password>Usage Examples:
import org.testcontainers.containers.PostgreSQLContainer;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
// Using PostgreSQLContainer with PgVector image
public class PgVectorTest {
public void testWithPgVector() throws Exception {
try (PostgreSQLContainer<?> pgvector = new PostgreSQLContainer<>("pgvector/pgvector:pg16")) {
pgvector.start();
String jdbcUrl = pgvector.getJdbcUrl();
try (Connection conn = DriverManager.getConnection(
jdbcUrl,
pgvector.getUsername(),
pgvector.getPassword())) {
try (Statement stmt = conn.createStatement()) {
// Enable vector extension
stmt.execute("CREATE EXTENSION IF NOT EXISTS vector");
// Create table with vector column
stmt.execute(
"CREATE TABLE embeddings (" +
"id SERIAL PRIMARY KEY, " +
"content TEXT, " +
"embedding vector(3))" // 3-dimensional vectors
);
// Create index for fast similarity search
stmt.execute(
"CREATE INDEX ON embeddings " +
"USING ivfflat (embedding vector_cosine_ops) " +
"WITH (lists = 100)"
);
// Insert vector data
String insertSql =
"INSERT INTO embeddings (content, embedding) VALUES (?, ?::vector)";
try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
pstmt.setString(1, "cat");
pstmt.setString(2, "[1, 2, 3]");
pstmt.executeUpdate();
pstmt.setString(1, "dog");
pstmt.setString(2, "[1, 2, 4]");
pstmt.executeUpdate();
pstmt.setString(1, "tree");
pstmt.setString(2, "[10, 20, 30]");
pstmt.executeUpdate();
}
// Vector similarity search (find nearest neighbors)
String searchSql =
"SELECT content, embedding, " +
"embedding <=> ?::vector AS distance " +
"FROM embeddings " +
"ORDER BY embedding <=> ?::vector " +
"LIMIT 2";
try (PreparedStatement pstmt = conn.prepareStatement(searchSql)) {
String queryVector = "[1, 2, 3.5]";
pstmt.setString(1, queryVector);
pstmt.setString(2, queryVector);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
String content = rs.getString("content");
String embedding = rs.getString("embedding");
double distance = rs.getDouble("distance");
// Most similar: "dog" (distance ~0.5), then "cat" (distance ~0.5)
}
}
}
// Different distance metrics
// <-> : Euclidean distance (L2)
// <=> : Cosine distance
// <#> : Inner product (negative)
// Cosine similarity search
try (ResultSet rs = stmt.executeQuery(
"SELECT content, " +
"1 - (embedding <=> '[1,2,3]'::vector) AS cosine_similarity " +
"FROM embeddings " +
"ORDER BY embedding <=> '[1,2,3]'::vector " +
"LIMIT 3")) {
while (rs.next()) {
String content = rs.getString("content");
double similarity = rs.getDouble("cosine_similarity");
// Ordered by similarity
}
}
}
}
}
}
}PgVector with JDBC URL Pattern:
// Automatic container management via JDBC URL
public void testPgVectorWithJdbcUrl() throws Exception {
String jdbcUrl = "jdbc:tc:pgvector:///testdb";
try (Connection conn = DriverManager.getConnection(jdbcUrl)) {
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE EXTENSION IF NOT EXISTS vector");
// Create table with higher-dimensional vectors
stmt.execute(
"CREATE TABLE documents (" +
"id SERIAL PRIMARY KEY, " +
"text TEXT, " +
"embedding vector(1536))" // OpenAI embedding dimension
);
// Use vector operations
// ...
}
}
}Available PgVector Versions:
pgvector/pgvector:pg16 - PgVector on PostgreSQL 16 (default)pgvector/pgvector:pg15 - PgVector on PostgreSQL 15pgvector/pgvector:pg14 - PgVector on PostgreSQL 14pgvector/pgvector:0.5.1-pg16 - Specific PgVector versionVector Operations:
<-> - Euclidean distance (L2)<=> - Cosine distance<#> - Inner product (negative)Index Types:
ivfflat - Inverted file with flat compression (faster build, less accurate)hnsw - Hierarchical Navigable Small World (slower build, more accurate)All variants can also be used directly with PostgreSQLContainer:
// PostGIS
DockerImageName postgisImage = DockerImageName
.parse("postgis/postgis:16-3.4")
.asCompatibleSubstituteFor("postgres");
PostgreSQLContainer<?> postgis = new PostgreSQLContainer<>(postgisImage);
// TimescaleDB
DockerImageName timescaleImage = DockerImageName
.parse("timescale/timescaledb:2.14.2-pg16")
.asCompatibleSubstituteFor("postgres");
PostgreSQLContainer<?> timescale = new PostgreSQLContainer<>(timescaleImage);
// PgVector (already compatible)
PostgreSQLContainer<?> pgvector = new PostgreSQLContainer<>("pgvector/pgvector:pg16");| Feature | Standard PostgreSQL | PostGIS | TimescaleDB | PgVector |
|---|---|---|---|---|
| Geographic queries | ❌ | ✅ | ❌ | ❌ |
| Time-series optimization | ❌ | ❌ | ✅ | ❌ |
| Vector similarity search | ❌ | ❌ | ❌ | ✅ |
| Standard SQL | ✅ | ✅ | ✅ | ✅ |
| JDBC support | ✅ | ✅ | ✅ | ✅ |
| R2DBC support | ✅ | ✅ | ✅ | ✅ |
All variant providers are registered via Java SPI in:
META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProviderRegistered providers:
org.testcontainers.containers.PostgreSQLContainerProviderorg.testcontainers.containers.PostgisContainerProviderorg.testcontainers.containers.TimescaleDBContainerProviderorg.testcontainers.containers.PgVectorContainerProviderUse Standard PostgreSQL when:
Use PostGIS when:
Use TimescaleDB when:
Use PgVector when:
Extension not found:
try (PostgreSQLContainer<?> postgis = new PostgreSQLContainer<>(
DockerImageName.parse("postgis/postgis:16-3.4")
.asCompatibleSubstituteFor("postgres"))) {
postgis.start();
try (Connection conn = DriverManager.getConnection(
postgis.getJdbcUrl(),
postgis.getUsername(),
postgis.getPassword());
Statement stmt = conn.createStatement()) {
// Verify PostGIS extension is available
try (ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM pg_available_extensions WHERE name = 'postgis'")) {
rs.next();
if (rs.getInt(1) == 0) {
throw new IllegalStateException("PostGIS extension not available in image");
}
}
// Enable extension with error handling
try {
stmt.execute("CREATE EXTENSION IF NOT EXISTS postgis");
} catch (SQLException e) {
System.err.println("Failed to enable PostGIS: " + e.getMessage());
System.err.println("Container logs: " + postgis.getLogs());
throw e;
}
}
}Extension installation failure:
try (PostgreSQLContainer<?> timescale = new PostgreSQLContainer<>(
DockerImageName.parse("timescale/timescaledb:2.14.2-pg16")
.asCompatibleSubstituteFor("postgres"))) {
timescale.start();
try (Connection conn = DriverManager.getConnection(
timescale.getJdbcUrl(),
timescale.getUsername(),
timescale.getPassword());
Statement stmt = conn.createStatement()) {
// Verify TimescaleDB extension
try (ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM pg_available_extensions WHERE name = 'timescaledb'")) {
rs.next();
if (rs.getInt(1) == 0) {
throw new IllegalStateException("TimescaleDB extension not available");
}
}
// Enable extension
stmt.execute("CREATE EXTENSION IF NOT EXISTS timescaledb");
// Verify extension is enabled
try (ResultSet rs = stmt.executeQuery(
"SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'")) {
if (!rs.next()) {
throw new IllegalStateException("TimescaleDB extension not enabled");
}
}
}
}Vector extension not available:
try (PostgreSQLContainer<?> pgvector = new PostgreSQLContainer<>("pgvector/pgvector:pg16")) {
pgvector.start();
try (Connection conn = DriverManager.getConnection(
pgvector.getJdbcUrl(),
pgvector.getUsername(),
pgvector.getPassword());
Statement stmt = conn.createStatement()) {
// Verify vector extension
try (ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM pg_available_extensions WHERE name = 'vector'")) {
rs.next();
if (rs.getInt(1) == 0) {
throw new IllegalStateException("Vector extension not available in image");
}
}
// Enable extension
stmt.execute("CREATE EXTENSION IF NOT EXISTS vector");
// Verify vector type is available
try (ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM pg_type WHERE typname = 'vector'")) {
rs.next();
if (rs.getInt(1) == 0) {
throw new IllegalStateException("Vector type not available");
}
}
}
}Handling incompatible images:
// Use asCompatibleSubstituteFor for variant images
DockerImageName postgisImage = DockerImageName
.parse("postgis/postgis:16-3.4")
.asCompatibleSubstituteFor("postgres");
try (PostgreSQLContainer<?> postgis = new PostgreSQLContainer<>(postgisImage)) {
postgis.start();
// Use container
} catch (IllegalArgumentException e) {
// Image compatibility issue
System.err.println("Image compatibility error: " + e.getMessage());
throw e;
}PostGIS verification:
try (PostgreSQLContainer<?> postgis = new PostgreSQLContainer<>(
DockerImageName.parse("postgis/postgis:16-3.4")
.asCompatibleSubstituteFor("postgres"))) {
postgis.start();
try (Connection conn = DriverManager.getConnection(
postgis.getJdbcUrl(),
postgis.getUsername(),
postgis.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("CREATE EXTENSION IF NOT EXISTS postgis");
// Verify PostGIS functions are available
try (ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM pg_proc WHERE proname LIKE 'st_%'")) {
rs.next();
int functionCount = rs.getInt(1);
assert functionCount > 0 : "PostGIS functions should be available";
}
}
}TimescaleDB verification:
try (PostgreSQLContainer<?> timescale = new PostgreSQLContainer<>(
DockerImageName.parse("timescale/timescaledb:2.14.2-pg16")
.asCompatibleSubstituteFor("postgres"))) {
timescale.start();
try (Connection conn = DriverManager.getConnection(
timescale.getJdbcUrl(),
timescale.getUsername(),
timescale.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("CREATE EXTENSION IF NOT EXISTS timescaledb");
// Verify TimescaleDB functions are available
try (ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM pg_proc WHERE proname LIKE 'create_hypertable'")) {
rs.next();
assert rs.getInt(1) > 0 : "TimescaleDB functions should be available";
}
}
}PgVector verification:
try (PostgreSQLContainer<?> pgvector = new PostgreSQLContainer<>("pgvector/pgvector:pg16")) {
pgvector.start();
try (Connection conn = DriverManager.getConnection(
pgvector.getJdbcUrl(),
pgvector.getUsername(),
pgvector.getPassword());
Statement stmt = conn.createStatement()) {
stmt.execute("CREATE EXTENSION IF NOT EXISTS vector");
// Verify vector operators are available
try (ResultSet rs = stmt.executeQuery(
"SELECT COUNT(*) FROM pg_operator WHERE oprname IN ('<->', '<=>', '<#>')")) {
rs.next();
assert rs.getInt(1) >= 3 : "Vector operators should be available";
}
}
}