The official PostgreSQL Docker images ship without extensions. Every time you need pgvector, PostGIS, or pg_cron, you're stuck manually installing dependencies, compiling from source, or settling for third-party images that bundle a fixed set of extensions.
pglayers fixes this. It's both a ready-to-use PostgreSQL distribution
with extensions pre-installed and a tool to build your own custom
image with exactly the extensions you need -- all on top of the
official postgres Docker images. You don't need to figure out how to
build PostgreSQL extensions, install dependencies, or set up compilers.
Each extension is published as a minimal Docker image layer containing
only its binaries. You stack them on top of the official postgres
image using COPY --from -- one line per extension, no compilation.
Quick start
Option 1: Ready-to-use images
Pre-built combined images with shared_preload_libraries already
configured:
# All 80+ extensions docker run -d -e POSTGRES_PASSWORD=secret ghcr.io/pglayers/pglayers-full:17 # Azure Database for PostgreSQL compatible (30+ extensions) docker run -d -e POSTGRES_PASSWORD=secret ghcr.io/pglayers/pglayers-azure:17
Available profiles: full, azure. Each is published for PG 17, 18,
and 19. See Profiles for details and how to create custom
ones.
The azure profile includes DocumentDB, providing MongoDB wire protocol
compatibility on port 10260 -- the same engine behind Azure DocumentDB.
Note: Vendor profiles (e.g.,
azure) are a best-effort approximation for local development. They are not a replacement for the actual managed service -- extension versions, configuration defaults, and platform-specific behavior may differ. Use them to develop and test locally, not to replicate production exactly.
Option 2: Pick your own extensions
Each extension is published as its own image layer. You stack them onto
the official postgres image with COPY --from -- each line adds one
extension to the final image:
FROM postgres:17 COPY --from=ghcr.io/pglayers/pgx-pgvector:17 / / COPY --from=ghcr.io/pglayers/pgx-pg_cron:17 / / COPY --from=ghcr.io/pglayers/pgx-postgis:17 / /
Build and run:
docker build -t my-postgres .
docker run -d -e POSTGRES_PASSWORD=secret my-postgresNo compilation happens -- Docker pulls the pre-built extension layers from the registry and overlays them onto the official image. The result is a single image with exactly the extensions you chose, composed layer by layer.
PG 18+ isolated layout
Starting with PostgreSQL 18, pglayers uses an isolated extension
layout that leverages PostgreSQL's extension_control_path GUC. Each
extension lives in its own /extensions/<name>/ namespace, eliminating
file collision risk entirely:
FROM postgres:18 COPY --from=ghcr.io/pglayers/pgx-pgvector:18 / /extensions/pgvector/ COPY --from=ghcr.io/pglayers/pgx-pg_cron:18 / /extensions/pg_cron/ COPY --from=ghcr.io/pglayers/pgx-postgis:18 / /extensions/postgis/ # Tell PostgreSQL where to find isolated extensions RUN echo "extension_control_path = '/extensions/pgvector/share:/extensions/pg_cron/share:/extensions/postgis/share:\$system'" \ >> /usr/share/postgresql/postgresql.conf.sample && \ echo "dynamic_library_path = '/extensions/pgvector/lib:/extensions/pg_cron/lib:/extensions/postgis/lib:\$libdir'" \ >> /usr/share/postgresql/postgresql.conf.sample
No linker configuration (ld.so.conf.d/ldconfig/LD_LIBRARY_PATH) is
needed: each isolated layer is self-contained. Every ELF object it ships
carries an $ORIGIN RUNPATH pointing at its own
/extensions/<ext>/lib, and any bundled runtime library it needs (e.g.
libcurl -> libssh2) has a per-extension-mangled soname
(pglx_<ext>_<soname>). This keeps each layer's dependencies private, so
two extensions that bundle the same library (for example postgis and
pg_duckdb, both of which pull in libssh2 via libcurl) can never bind
to each other's copy in the shared postgres process. Configuring a global
linker path instead would collapse every soname to a single winner and
reintroduce exactly that cross-layer collision.
The $system and $libdir suffixes ensure built-in contrib extensions
(hstore, pg_stat_statements, etc.) remain discoverable.
Note: The pre-built profile images (
pglayers-full:18,pglayers-azure:18) handle all GUC and linker configuration automatically. The manual setup above is only needed when composing your own image with Option 2.
New to Docker? See Verifying your container for how to check it's running and handle port conflicts.
Supported PostgreSQL versions
| Version | Status |
|---|---|
| PostgreSQL 17 | Stable |
| PostgreSQL 18 | Stable |
| PostgreSQL 19 | Experimental (beta -- supported until officially released) |
All extensions are built and tested against PG 17 and 18. Support for PG 19 is best-effort while it remains in beta; some extensions may not yet have upstream compatibility. Once PG 19 reaches GA, it will be promoted to stable.
Available extensions
| Extension | Version | PG versions | Description |
|---|---|---|---|
| age | 1.7.0 (PG17), 1.8.0 (PG18) | 17, 18 | Graph database with openCypher query language (Apache AGE) |
| anon | 3.1.1 | 17, 18 | Data anonymization and masking |
| credcheck | 5.0 | 17, 18, 19 | Credential checks on user creation / password change |
| db2fce | 0.0.17 | 17, 18 | DB2 compatibility functions (date/time, string helpers) |
| documentdb | 0.114-0 | 17, 18 | MongoDB-compatible document database engine (BSON types and CRUD API) |
| extra_window_functions | 1.0 | 17, 18, 19 | Extra window functions (ignore-nulls variants, nth-from-last) |
| first_last_agg | 0.1.4-4-gd63ea3b | 17, 18 | first() and last() aggregate functions |
| h3-pg | 4.2.3 | 17, 18 | Uber H3 hexagonal geospatial indexing |
| hll | 2.21 | 17, 18, 19 | HyperLogLog probabilistic distinct counting |
| http | 1.7.2 | 17, 18, 19 | HTTP client for PostgreSQL (web requests from SQL) |
| hypopg | 1.4.3 | 17, 18, 19 | Hypothetical indexes for what-if analysis |
| icu_ext | 1.11.0 | 17, 18, 19 | ICU functions: Unicode names, transliteration, spellout, collation |
| ip4r | 2.4.3 | 17, 18, 19 | IPv4/IPv6 range data types with GiST indexing |
| jsquery | 1.2 | 17, 18 | JSON query language with GIN indexing |
| orafce | 4.16.7 | 17, 18, 19 | Oracle compatibility functions and packages |
| periods | 1.2.3 | 17, 18 | SQL:2016 application-time PERIODs and system versioning |
| pg_background | 2.0.2 | 17, 18, 19 | Run SQL in background worker processes |
| pg_csv | 1.0.2 | 17, 18, 19 | Aggregate result rows into CSV text |
| pg_dirtyread | 2.8 | 17, 18, 19 | Read dead (deleted, unvacuumed) tuples for forensics/recovery |
| pg_pwhash | 1.0 | 17, 18, 19 | Password hashing (scrypt, argon2, yescrypt) |
| pg_show_plans | 2.1.8 | 17, 18, 19 | Show execution plans of all currently running queries |
| pg_statviz | 1.1 | 17, 18, 19 | Time-series snapshots of PostgreSQL statistics for visualization |
| pgaudit | 17.1 | 17, 18 | Audit logging (session and object-level) |
| pg_bigm | 1.2 | 17, 18 | 2-gram full text search (better for CJK languages) |
| pg_clickhouse | 0.10.0 | 17, 18 | Query ClickHouse databases from PostgreSQL (FDW with pushdown; depends on re2 for regex pushdown) |
| pg_cron | 1.6.7 | 17, 18 | Job scheduler (periodic jobs inside the database) |
| pg_duckdb | 1.1.1 | 17, 18 | DuckDB columnar analytics engine embedded in Postgres |
| pg_durable | 0.2.3 | 17, 18 | In-database durable execution (fault-tolerant workflows) |
| pg_failover_slots | 1.2.1 | 17, 18 | Logical replication slot manager for failover |
| pg_graphql | 1.6.1 | 17, 18 | GraphQL support for PostgreSQL |
| pg_hashids | 1.2.1 | 17, 18, 19 | Short unique hash IDs from integers |
| pg_hint_plan | 1.7.1 | 17, 18 | Tweak execution plans using hints in SQL comments |
| pg_ivm | 1.13 | 17, 18 | Incremental View Maintenance for materialized views |
| pg_jsonschema | 0.3.4 | 17, 18 | JSON Schema validation |
| pg_lake | 3.4.1 | 17 | Iceberg and data lake access (Parquet, CSV, JSON via DuckDB) |
| pg_net | 0.20.3 | 17, 18, 19 | Async non-blocking HTTP/HTTPS requests |
| pg_partman | 5.4.3 | 17, 18, 19 | Automated table partition management |
| pg_permissions | 1.4.1 | 17, 18, 19 | Review and audit object permissions against a desired state |
| pg_qualstats | 2.1.4 | 17, 18, 19 | Statistics collector for WHERE clause predicates |
| pg_re2 | 0.4.1 | 17, 18, 19 | ClickHouse-compatible regex functions using RE2 |
| pg_repack | 1.5.3 | 17, 18, 19 | Online table reorganization without heavy locks |
| pg_roaringbitmap | 1.2.0 | 17, 18 | Roaring bitmap data type for fast set operations |
| pg_rrule | 0.3.0 | 17, 18, 19 | iCalendar RRULE recurrence type and occurrence expansion |
| pg_similarity | 1.0 | 17, 18 | Similarity functions (Levenshtein, Jaro-Winkler, Cosine, Jaccard) |
| pg_squeeze | 1.9.3 | 17, 18, 19 | Remove unused space from tables without heavy locks |
| pg_stat_monitor | 2.3.2 | 17, 18 | Enhanced query statistics with histograms and buckets |
| pg_textsearch | 1.3.1 | 17, 18 | BM25 relevance-ranked full-text search |
| pg_uuidv7 | 1.7.0 | 17, 18, 19 | UUIDv7 generation (time-sortable unique identifiers) |
| pg_wait_sampling | 1.1.11 | 17, 18, 19 | Sampling-based statistics of wait events |
| pgfincore | 1.4.0 | 17, 18, 19 | Inspect and manage OS page cache for data files |
| pgjwt | master | 17, 18, 19 | JSON Web Token (JWT) generation and validation |
| pglogical | 2.4.7 | 17, 18, 19 | Logical streaming replication using publish/subscribe model |
| pgmq | 1.12.0 | 17, 18, 19 | Lightweight message queue on Postgres (like AWS SQS/RSMQ) |
| pgnodemx | 2.0.1 | 17, 18 | Expose node OS/cgroup metrics as SQL (container-aware monitoring) |
| pgpcre | 0.20190509 | 17, 18, 19 | Perl-compatible regular expression (PCRE) type and functions |
| pgrouting | 4.0.1 | 17, 18, 19 | Geospatial routing and network analysis on PostGIS |
| pgsodium | 3.1.11 | 17, 18, 19 | Modern cryptography using libsodium |
| pgsphere | 1.5.2 | 17, 18 | Spherical data types (points, circles, polygons) for astronomy/geo |
| pgtap | 1.3.4 | 17, 18, 19 | Unit testing framework for PostgreSQL |
| pgtt | 4.5 | 17, 18, 19 | Oracle-style Global Temporary Tables |
| pgvector | 0.8.5 | 17, 18, 19 | Vector similarity search for AI/embeddings |
| pgvectorscale | 0.9.0 | 17, 18 | High-performance vector search with DiskANN (complements pgvector) |
| pljs | 1.0.5 | 17, 18 | JavaScript (QuickJS) procedural language |
| plpgsql_check | 2.10.1 | 17, 18, 19 | PL/pgSQL linter and validator |
| plprofiler | 4.2.5 | 17, 18 | Performance profiler for PL/pgSQL functions |
| plv8 | 3.2.4 | 17, 18 | JavaScript (V8) procedural language |
| PostGIS | 3.6.4 | 17, 18, 19 | Geospatial extensions (geometry, geography, raster, MVT) |
| postgres_protobuf | 0.3.2 | 17, 18, 19 | Protocol Buffer support (query, convert to/from JSON) |
| prefix | 1.2.11 | 17, 18, 19 | Prefix range data type for phone routing lookups |
| prioritize | 1.0.4 | 17, 18 | Get/set OS scheduling priority of backend processes |
| rational | 0.0.2 | 17, 18, 19 | Precise fractional (rational number) arithmetic |
| rum | 1.3.15 | 17, 18 | GIN-like index with ordering for full text search |
| semver | 0.41.0 | 17, 18, 19 | Semantic version data type |
| set_user | 4.2.0 | 17, 18, 19 | Auditable privilege escalation control (set_user/reset_user) |
| tdigest | 1.4.4 | 17, 18, 19 | T-digest for quantile and percentile estimation |
| tds_fdw | 2.0.5 | 17, 18 | Foreign data wrapper for SQL Server and Sybase |
| temporal_tables | 1.2.2 | 17, 18 | System-period temporal tables |
| timescaledb | 2.28.2 | 17, 18 | Time-series hypertables, compression, continuous aggregates |
| timestamp9 | 1.4.0 | 17, 18, 19 | Nanosecond-precision timestamp type |
| toastinfo | 1.7 | 17, 18, 19 | Inspect the TOAST storage details of a value |
| wal2json | 2.6 | 17, 18 | JSON output plugin for logical replication / CDC |
| wrappers | 0.6.2 | 17, 18 | Foreign Data Wrapper framework (Stripe, S3, Firebase, etc.) |
Image tags
Each extension is published with two tag formats:
pgx-<extension>:<pg_major>-- latest build (e.g.pgx-pgvector:17)pgx-<extension>:<pg_major>-<version>-- pinned version (e.g.pgx-pgvector:17-v0.8.3)
All images are multi-architecture (linux/amd64 and linux/arm64) and
hosted on GHCR at ghcr.io/pglayers/pgx-*. Docker automatically pulls
the correct architecture for your platform.
Configuration notes
shared_preload_libraries
Some extensions require entries in shared_preload_libraries. Add this to
your Dockerfile after the COPY lines:
RUN echo "shared_preload_libraries = 'pg_cron,pgaudit,pg_partman_bgw'" \ >> /usr/share/postgresql/postgresql.conf.sample
Extensions that need this:
| Extension | Library name |
|---|---|
| age | age |
| anon | anon |
| credcheck | credcheck |
| documentdb | pg_documentdb_gw_host |
| pg_cron | pg_cron |
| pg_duckdb | pg_duckdb |
| pg_durable | pg_durable |
| pg_failover_slots | pg_failover_slots |
| pg_hint_plan | pg_hint_plan |
| pg_lake | pg_extension_base |
| pg_net | pg_net |
| pg_partman | pg_partman_bgw |
| pg_qualstats | pg_qualstats |
| pg_show_plans | pg_show_plans |
| pg_squeeze | pg_squeeze |
| pg_stat_monitor | pg_stat_monitor |
| pg_textsearch | pg_textsearch |
| pg_wait_sampling | pg_wait_sampling |
| pgaudit | pgaudit |
| pglogical | pglogical |
| pgnodemx | pgnodemx |
| pgsodium | pgsodium |
| pgtt | pgtt |
| plprofiler | plprofiler |
| set_user | set_user |
| timescaledb | timescaledb |
CREATE EXTENSION
Extensions must be created in each database where you want to use them. You can automate this with an init script:
COPY <<'EOF' /docker-entrypoint-initdb.d/10-extensions.sql CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_cron; CREATE EXTENSION IF NOT EXISTS postgis; EOF
This runs automatically on first container start (when the data directory is initialized).
PostGIS
The PostGIS extension image bundles its runtime shared libraries (libgeos,
libproj, libgdal, libjson-c, libprotobuf-c, and PROJ/GDAL data files), so
COPY --from is fully self-contained. Geometry, geography, topology,
raster, and MVT (Mapbox Vector Tiles) are all included.
To enable GDAL raster format drivers (GeoTIFF, PNG, etc.), set the environment variable:
ENV POSTGIS_GDAL_ENABLED_DRIVERS=ENABLE_ALLBy default, GDAL drivers are disabled for security (same as the official PostGIS Docker image).
DocumentDB
DocumentDB provides a MongoDB-compatible document database engine built on PostgreSQL. It consists of two extensions:
documentdb_core-- BSON data type and core operations (no dependencies, works standalone).documentdb-- Full CRUD API surface. Requiresdocumentdb_core,pg_cron,vector(pgvector),postgis, andtsm_system_rows.
The layer also includes pg_documentdb_gw_host, a background worker
that provides MongoDB wire protocol compatibility on port 10260. When
loaded via shared_preload_libraries, MongoDB clients (mongosh, pymongo,
Node.js driver) can connect directly.
Create extensions in order:
CREATE EXTENSION IF NOT EXISTS documentdb_core;
-- For the full API (requires pg_cron, vector, postgis layers):
CREATE EXTENSION IF NOT EXISTS documentdb;Gateway configuration (add to postgresql.conf):
shared_preload_libraries = 'pg_documentdb_gw_host' documentdb_gateway.database = 'postgres' documentdb_gateway.setup_configuration_file = '/etc/documentdb/gateway_config.json'
A default configuration file is bundled in the layer at
/etc/documentdb/gateway_config.json. It listens on port 10260 with
auto-generated self-signed TLS certificates (clients can connect with or
without TLS). Override with your own file to customize ports, TLS, or
blocked roles.
On the PG 18+ isolated layout the bundled config lives at
/extensions/documentdb/etc/documentdb/gateway_config.json; the combined profile images add a/etc/documentdb/gateway_config.jsonsymlink to it so the gateway finds it. In those profile images thedocumentdbextension is also auto-created on first init (see Profile images), so the gateway comes up clean without the "extension not yet created" retries. SetPGLAYERS_CREATE_EXTENSIONS=noneto opt out.
pg_lake
pg_lake provides Iceberg table support and data lake file access (Parquet, CSV, JSON) for PostgreSQL. It uses DuckDB as its query engine via a companion process.
The layer includes:
- Multiple PostgreSQL extensions (
pg_lake,pg_lake_table,pg_lake_engine,pg_lake_iceberg,pg_lake_copy,pg_extension_base,pg_map) pgduck_serverbinary (standalone DuckDB-backed process)- Auto-start entrypoint wrapper
Setup:
FROM postgres:17 COPY --from=ghcr.io/pglayers/pgx-pg_lake:17 / / RUN echo "shared_preload_libraries = 'pg_extension_base'" \ >> /usr/share/postgresql/postgresql.conf.sample ENTRYPOINT ["/usr/local/bin/pg-lake-entrypoint.sh"] CMD ["postgres"]
The entrypoint wrapper starts pgduck_server in the background (Unix
socket on port 5332, no external port exposed) then delegates to the
standard postgres entrypoint. No manual process management required.
CREATE EXTENSION pg_lake CASCADE;
Note: pg_lake conflicts with pg_duckdb (both bundle libduckdb.so).
Do not use both in the same image.
PostgreSQL 17 only. pg_lake requires its pg_extension_base shim in
shared_preload_libraries. That shim installs a ProcessUtility hook
that intercepts every CREATE EXTENSION and looks up the target's
control file in the hardcoded $system sharedir, ignoring PostgreSQL
18's extension_control_path. Because the PG 18+ isolated layout
installs every extension under /extensions/<ext>/share and resolves
them purely via extension_control_path, preloading pg_extension_base
would break CREATE EXTENSION for every extension in the image (and
pg_lake itself). pg_lake is therefore built only for the PG 17 classic
layout until upstream teaches pg_extension_base to honor
extension_control_path. See
issue #37 and upstream
Snowflake-Labs/pg_lake#494.
Profile images (auto-configuration)
The pre-built combined images (pglayers-full, pglayers-azure)
automatically handle all runtime configuration:
-
shared_preload_libraries-- set from each extension'sSHARED_PRELOADfield. -
max_worker_processes-- raised to64(the stock default of8is far too low once a dozen background-worker extensions are preloaded, and causestoo many background workerserrors and a runaway TimescaleDB launcher retry loop). -
postgresql.conf settings -- GUC parameters required by extensions (e.g.,
documentdb_gateway.database) are appended automatically from each extension'sPG_CONFfield. This includes noise-suppressing defaults such aspgnodemx.kdapi_enabled = off(no Kubernetes Downward API outside k8s) andcron.log_statement = off(don't echo every scheduled job to the server log). -
Companion processes -- extensions that need a background process (e.g., pg_lake's
pgduck_server) are started automatically via a generated entrypoint wrapper. No manual process management required. -
Auto-created extensions -- extensions that ship an always-on background worker or wire-protocol gateway which polls for their SQL objects (currently documentdb) are created automatically on first init via
CREATE EXTENSION ... CASCADE, so a fresh container starts without perpetual "not yet created" warnings. Override with thePGLAYERS_CREATE_EXTENSIONSenv var (a space/comma-separated list of SQL extension names, ornoneto disable):# disable auto-creation entirely docker run -e PGLAYERS_CREATE_EXTENSIONS=none ghcr.io/pglayers/pglayers-full:18 # auto-create a different set docker run -e PGLAYERS_CREATE_EXTENSIONS="vector,postgis" ghcr.io/pglayers/pglayers-full:18
When using profile images, you don't need to configure any of the above
manually -- just docker run and CREATE EXTENSION.
How it works
The project publishes one Docker image per extension per PostgreSQL
version. These are not runnable containers -- they are FROM scratch
images containing only the extension artifacts.
How extensions are built
Extensions fall into four build families (in order of preference):
- APT via the shared template -- the default for most extensions.
They have no Dockerfile at all: a single shared
Dockerfile.aptinstallspostgresql-<pg>-<pkg>from PGDG, extracts the files, bundles any non-base runtime libraries, and relocates them for the classic layout. You only write anextension.conf(withAPT_PACKAGE) and atest.sql. (~56 extensions, e.g. pgvector, pg_cron.) - APT with a custom Dockerfile -- for apt packages the shared template
can't express: multiple/renamed packages,
.controlupdate-alternatives symlinks, or extra steps. (e.g. postgis, pgrouting, http, h3_pg, tds_fdw.) - Source-built from an upstream prebuilt image -- for heavy builds
that ship an official image or a cached build stage, avoiding a long
compile in CI. (e.g. pg_duckdb from
pgduckdb/pgduckdb, pg_lake from a prebuilt vcpkg image.) - Source-built from git --
git clone+makeat a pinned upstream tag, when no apt package exists. (e.g. pg_net, pgsodium, the pgrx/Rust extensions.)
Whatever the family, every layer must be self-contained (carry all of its own non-base runtime libraries) and collision-free -- enforced by the test suite. See AGENTS.md for the build requirements and CONTRIBUTING.md for how to add one.
PG 17: Classic layout
Extension files are at their standard PostgreSQL filesystem paths:
/usr/lib/postgresql/17/lib/vector.so
/usr/share/postgresql/17/extension/vector.control
/usr/share/postgresql/17/extension/vector--0.8.4.sql
When you write COPY --from=ghcr.io/pglayers/pgx-pgvector:17 / / in
your Dockerfile, Docker copies these files into the official postgres
image at exactly the right locations. PostgreSQL finds them and you can
CREATE EXTENSION.
PG 18+: Isolated layout
Extension files use a flat layout compatible with PostgreSQL's
extension_control_path GUC and CloudNativePG ImageVolumes:
/lib/vector.so
/lib/bitcode/vector/...
/share/extension/vector.control
/share/extension/vector--0.8.4.sql
Each extension image is mounted into its own namespace
(/extensions/<name>/) in the combined image. PostgreSQL discovers them
via the extension_control_path and dynamic_library_path GUCs. This
approach:
- Eliminates file collisions -- two extensions can bundle different versions of the same library without conflict
- Enables runtime composability -- extensions can be mounted at deploy time via Docker volumes or Kubernetes ImageVolumes
- Is CNPG-native -- pglayers
:18images are directly usable as CloudNativePGClusterImageCatalogentries without modification
CloudNativePG usage
pglayers PG 18+ images are directly compatible with CloudNativePG ImageVolume extensions (requires CNPG >= 1.27 and Kubernetes >= 1.33).
Important: OS compatibility. pglayers extensions are built against
postgres:18(Debian Trixie, glibc 2.38). When using CNPG, the operand image must also be Trixie-based:imageName: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixieThe Bullseye/Bookworm variants (
ghcr.io/cloudnative-pg/postgresql:18) have an older glibc and will fail withGLIBC_2.38 not found. This is the same constraint that applies to CNPG's own extension images -- extensions must match the operand's OS distribution.
Generate a ClusterImageCatalog for your cluster:
make cnpg-catalog PG=18 REGISTRY=ghcr.io/pglayers > catalog.yaml
kubectl apply -f catalog.yamlOr for a specific profile:
make cnpg-catalog PG=18 PROFILE=azure REGISTRY=ghcr.io/pglayers > catalog-azure.yamlThen reference extensions in your CNPG Cluster spec:
apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: my-cluster spec: instances: 3 imageName: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie imageCatalogRef: apiGroup: postgresql.cnpg.io kind: ClusterImageCatalog name: pglayers major: 18 postgresql: shared_preload_libraries: - pg_cron extensions: - name: pgvector - name: pg-cron - name: postgis ld_library_path: - lib
You can also define extensions directly without a catalog:
postgresql: extensions: - name: pgvector image: reference: ghcr.io/pglayers/pgx-pgvector:18-0.8.4 - name: postgis image: reference: ghcr.io/pglayers/pgx-postgis:18-3.6.4 ld_library_path: - lib
Extensions with bundled runtime dependencies (PostGIS, pgRouting) need
ld_library_path: ["lib"] so the system linker can find their shared
libraries at /extensions/<name>/lib/.
Kubernetes ImageVolumes (without CNPG)
pglayers extension images also work as
Kubernetes ImageVolumes
with the stock postgres:18 image -- no operator required. This gives
you runtime extension composition on any Kubernetes 1.33+ cluster:
apiVersion: v1 kind: Pod metadata: name: postgres-with-extensions spec: volumes: - name: ext-pgvector image: reference: ghcr.io/pglayers/pgx-pgvector:18-0.8.4 - name: ext-postgis image: reference: ghcr.io/pglayers/pgx-postgis:18-3.6.4 - name: ext-pg-cron image: reference: ghcr.io/pglayers/pgx-pg_cron:18-v1.6.7 containers: - name: postgres image: postgres:18 env: - name: POSTGRES_PASSWORD value: "secret" args: - "postgres" - "-c" - "extension_control_path=/extensions/pgvector/share:/extensions/pg_cron/share:/extensions/postgis/share:$$system" - "-c" - "dynamic_library_path=/extensions/pgvector/lib:/extensions/pg_cron/lib:/extensions/postgis/lib:$$libdir" - "-c" - "shared_preload_libraries=pg_cron" volumeMounts: - name: ext-pgvector mountPath: /extensions/pgvector readOnly: true - name: ext-postgis mountPath: /extensions/postgis readOnly: true - name: ext-pg-cron mountPath: /extensions/pg_cron readOnly: true
Each extension image is mounted as a read-only volume at
/extensions/<name>/. PostgreSQL discovers them via
extension_control_path and dynamic_library_path. No image rebuild
needed -- add or remove extensions by changing volume definitions.
Requirements: Kubernetes 1.33+ (ImageVolume GA). The
$$systemand$$libdirsuffixes ensure built-in contrib extensions remain available.
OCI image labels
Every extension image includes standard OCI labels for machine-readable discovery:
| Label | Description |
|---|---|
org.opencontainers.image.title |
Extension name |
org.opencontainers.image.description |
Short description |
org.opencontainers.image.version |
Extension version |
org.opencontainers.image.source |
Upstream repository URL |
org.opencontainers.image.licenses |
SPDX license identifier |
io.pglayers.pg.major |
PostgreSQL major version |
io.pglayers.layout |
classic or isolated |
io.pglayers.extension.name |
Extension name |
io.pglayers.extension.version |
Extension version |
Query labels with:
docker inspect ghcr.io/pglayers/pgx-pgvector:18 --format '{{json .Config.Labels}}'Building locally
Prerequisites
- Docker (with BuildKit)
- GNU Make
- Bash
Build commands
# List available extensions with descriptions and PG versions make list # Build a single extension for a specific PG version make build EXT=pgvector PG=17 # Build all extensions for a PG version make build-all PG=17 # Build for PG 19 (beta -- requires PG_TAG override until GA) make build EXT=pgvector PG=19 PG_TAG=19beta3 make build-all PG=19 PG_TAG=19beta3 # Build a combined image with ALL extensions included make image PG=17 REGISTRY=local # Custom image name make image PG=18 REGISTRY=local IMAGE_NAME=my-postgres # Show detailed info for an extension (versions, notes, preload reqs) make info EXT=pg_cron # Push a built extension image to the registry make push EXT=pgvector PG=17 # Push all extensions make push-all PG=17 # Override the default registry make build EXT=pgvector PG=17 REGISTRY=ghcr.io/myorg # Print the Dockerfile for an extension (useful for debugging) make dockerfile EXT=pgvector # Remove built image for a single extension make clean EXT=pgvector # Remove all built extension images (reclaim disk space) make clean-all # Scaffold a new APT-based extension (probes PGDG, detects license, # writes extension.conf + a test.sql stub) make add-apt-ext PKG=<apt-package> [NAME=<dir>] [PG=17] # Verify all extensions comply with the licensing policy make check-licenses # Verify profiles are in sync with the extensions/ directory make check-profiles
APT-based extensions have no Dockerfile -- they are built by the shared
Dockerfile.apt, with their version and PostgreSQL-version support resolved from PGDG at build time. Adding one is usually justmake add-apt-ext+ writingtest.sql. See the Contributing guide.
Skipping extensions from CI
Extensions with CI_SKIP=1 in their extension.conf are excluded
from CI builds but remain in the repository for local builds. This is
useful for extensions with prohibitively long build times (e.g., plv8
whose V8 engine compilation exceeds CI timeouts on arm64 emulation).
# Still works locally make build EXT=plv8 PG=17 # But skipped by CI (not in build matrix, not in profile images)
To skip an extension, add CI_SKIP=1 to its extension.conf. To
re-enable, remove the field and add the extension back to the
appropriate profiles.
Running tests
# Full test suite: collisions, ldd, CREATE EXTENSION, smoke tests, # integration tests (builds all extensions first) make test REGISTRY=local PG=17 # Quick integration tests against an already-built combined image make image PG=17 REGISTRY=local make test-image PG=17 # Kubernetes ImageVolume integration test (requires k3d, PG 18+) make test-k8s REGISTRY=local PG=18
Tests must pass for all supported PG versions (17, 18, 19).
Profiles
Profiles let you build and test a curated subset of extensions rather than the full set. This is useful for matching managed PostgreSQL service offerings (e.g., Azure Database for PostgreSQL) or creating purpose-built images.
# List available profiles make list-profiles # List extensions in a profile make list PROFILE=azure # Build only the extensions in a profile make build-all PG=17 PROFILE=azure # Build a combined image with only the profile's extensions make image PG=17 PROFILE=azure # produces: pglayers-azure:17 # Run the full test suite against a profile make test REGISTRY=local PG=17 PROFILE=azure # Validate all profile files are in sync with extensions/ make check-profiles
Shipped profiles
| Profile | Description |
|---|---|
full |
All extensions provided by pglayers |
azure |
Extensions matching Azure Database for PostgreSQL Flexible Server |
Creating a custom profile
Create a text file in profiles/ with one extension directory name per
line (alphabetically sorted). Comments (#) and blank lines are ignored:
# My custom profile
pg_cron
pgvector
postgis
Then use it:
make image PG=17 PROFILE=myprofile
make test REGISTRY=local PG=17 PROFILE=myprofilePublished profile images
CI builds and pushes combined profile images to GHCR:
ghcr.io/<owner>/pglayers-azure:17ghcr.io/<owner>/pglayers-azure:18ghcr.io/<owner>/pglayers-full:17ghcr.io/<owner>/pglayers-full:18
These are ready-to-use PostgreSQL images with the profile's extensions
pre-installed and shared_preload_libraries configured.
The make test suite checks:
-
No file collisions -- Every pair of extensions is compared for overlapping files. If two extensions install a file at the same path, the last
COPY --fromsilently overwrites the first. Docker gives zero warning about this, but it can break extensions at runtime. -
No base image overwrites -- Extensions must not replace files from the official
postgres:XXimage. -
Shared library dependencies --
lddis run on every.soin the combined image to catch missing transitive dependencies. -
CREATE EXTENSION -- Every extension loads successfully in the combined image.
-
Functional smoke tests -- Each extension is exercised with a real query (not just loaded) to verify runtime behavior.
-
Integration tests -- Each extension's
test.sqlfile runs multi-step validation with PASS/FAIL assertions.
Example output:
---- Phase 3: Checking for file collisions between extensions...
PASS pgvector <-> postgis: no collisions
...
PASS No file collisions detected between any extension pair
---- Phase 5: Building combined image and checking shared libraries...
PASS All shared library dependencies resolve
---- Phase 6: Functional tests...
PASS CREATE EXTENSION vector
PASS smoke: pgvector similarity
...
---- Phase 7: Integration tests (extensions/*/test.sql)...
PASS integration pgvector (3 checks)
PASS integration postgis (4 checks)
...
========================================
Results: 699 passed, 0 failed, 0 warnings
========================================
Verifying your container
After running a docker run command, the container starts in the
background (-d flag). To confirm it's running:
# List running containers -- look for STATUS "Up" docker ps # Check the container logs for "database system is ready to accept connections" docker logs <container_id>
To connect and verify PostgreSQL is working:
# Connect from inside the running container docker exec -it <container_id> psql -U postgres -c "SELECT version();"
Replace <container_id> with the ID shown by docker ps (or the first
few characters of it).
Exposing and changing the port
By default, the examples above don't publish a port to your host machine.
To access PostgreSQL from outside the container, add -p:
# Publish pglayers (PostgreSQL) on the default port (5432)
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret ghcr.io/pglayers/pglayers-full:17If port 5432 is already in use (another PostgreSQL instance, for example), map to a different host port:
# Use host port 5433 instead (container still listens on 5432 internally)
docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=secret ghcr.io/pglayers/pglayers-full:17Then connect specifying the port:
psql -h localhost -p 5433 -U postgres
The format is -p <host_port>:<container_port>. Only the host port
(left side) needs to change -- the container always listens on 5432.
Contributing
See CONTRIBUTING.md for the full guide on adding extensions, reporting bugs, and submitting changes.
Project structure
pglayers/
├── Makefile Build interface
├── Dockerfile Combined image (all extensions)
├── Dockerfile.apt Shared build for all APT extensions
├── CONTRIBUTING.md Contribution guide
├── AGENTS.md Agent/CI instructions
├── scripts/
│ ├── apt-support.sh PGDG availability + version probing
│ ├── apt-lock.sh Generate .github/apt-versions.json
│ ├── ext-version.sh Resolve an extension's build version
│ ├── detect-license.sh License detection from Debian copyright
│ ├── check-licenses.sh Enforce the licensing policy
│ └── licenses.conf Allow/deny/exception license lists
├── .github/
│ ├── base-image-digests.json Tracked base image digests
│ ├── apt-versions.json Recorded apt versions (per PG major)
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml Bug report form
│ │ ├── new_extension.yml Extension request form
│ │ └── config.yml Template chooser config
│ └── workflows/
│ ├── ci.yml CI: build + test + compose profiles; publish on merge
│ ├── monitor-base-image.yml Detects base image updates (every 6h)
│ ├── monitor-extensions.yml Detects new source-build releases
│ ├── monitor-apt-versions.yml Tracks apt versions (apt-versions.json)
│ └── cache-cleanup.yml Prunes stale GHA caches
├── extensions/
│ ├── pgvector/ APT extension: conf + test only
│ │ ├── extension.conf Metadata (APT_PACKAGE, license, ...)
│ │ └── test.sql Integration tests (PASS/FAIL assertions)
│ ├── pg_net/ Source-built: adds its own Dockerfile
│ ├── postgis/ Custom APT Dockerfile (bundles libs)
│ ├── ... (80+ extensions)
│ └── wal2json/
├── profiles/
│ ├── azure.txt Azure PostgreSQL Flexible Server extensions
│ └── full.txt All extensions (CI-verified)
├── tests/
│ ├── test-layers.sh Full test suite (collisions + functional)
│ └── test-image.sh Quick integration tests against combined image
└── examples/
└── Dockerfile.example End-user reference
Licensing policy
This project ships extensions with permissive open-source licenses (PostgreSQL, MIT, ISC, Zlib, Apache-2.0, the BSD family, plus the safe weak/file-level copyleft MPL-2.0 and permissive-classified Artistic-2.0) and, where industry practice clearly supports it, GPL-2.0 extensions loaded via PostgreSQL's dynamic extension mechanism.
The policy is codified in scripts/licenses.conf
and enforced automatically by make check-licenses in CI: source-available
licenses (BSL/BUSL, SSPL, FSL, Elastic-2.0/ELv2) and infectious copyleft
(GPL/LGPL/AGPL) are denied, with postgis and pgrouting recorded as
documented, deliberate exceptions. Every extension's license is
auto-detected from its Debian copyright when it is added (see the
Contributing guide).
GPL-2.0 extensions (PostGIS, pgRouting)
PostGIS and pgRouting are licensed under GPL-2.0. Strictly interpreted,
the GPL could apply to any program that "links" with GPL code. However,
PostgreSQL extensions are loaded at runtime via dlopen() through a
stable public API (CREATE EXTENSION), which the PostgreSQL community
and broader industry treat as "mere aggregation" rather than
creating a combined work:
- Every managed PostgreSQL service (AWS RDS, Azure, Google Cloud SQL, Neon, Supabase) distributes PostGIS and pgRouting the same way.
- The official
postgis/postgisDocker image on Docker Hub uses the identicalCOPY --frompattern. - No GPL enforcement action has ever been taken against distributors of dynamically-loaded PostgreSQL extensions.
- The PostgreSQL project has operated under this interpretation for 20+ years.
We include these extensions because the practical risk is zero and
excluding them would make the project significantly less useful.
If your legal team disagrees with this interpretation, simply omit
the PostGIS and pgRouting COPY --from lines from your Dockerfile.
Extensions we exclude
| License | Policy | Examples |
|---|---|---|
| GPL-3.0 | Excluded (stronger copyleft, less industry consensus) | login_hook, session_variable |
| AGPL-3.0 | Excluded | topn |
| BSL, SSPL, ELv2, FSL | Excluded (not open source) | -- |
| Requires proprietary deps | Excluded | oracle_fdw (Oracle Instant Client) |
What this means in practice
| License | Included? | Examples |
|---|---|---|
| PostgreSQL, MIT, BSD, Apache 2.0, ISC | Yes | pgvector, pg_cron, pgaudit, timescaledb |
| MPL-2.0 (weak file-level copyleft) | Yes | pg_uuidv7 |
| GPL-2.0 (dynamic extension loading) | Yes | PostGIS, pgRouting |
| GPL-3.0 | No | login_hook, session_variable |
| AGPL-3.0 | No | -- |
| BSL, SSPL, ELv2, FSL | No | -- |
FAQ
How does pglayers work?
Each extension is built as a minimal FROM scratch Docker image containing
only the extension's files (shared libraries, control files, SQL scripts).
These images are not runnable containers -- they are filesystem layers.
When you write:
COPY --from=ghcr.io/pglayers/pgx-pgvector:17 / /Docker copies the extension files into the official postgres image at
exactly the right paths (/usr/lib/postgresql/17/lib/,
/usr/share/postgresql/17/extension/). PostgreSQL discovers them and
you can CREATE EXTENSION. No compilation, no package manager, no
runtime dependencies to resolve -- just file copies stacked on top of
the official image.
Why is version X of extension Y not available?
pglayers follows a deliberate version policy. We prefer stability and reproducibility over bleeding-edge releases, using this priority order:
-
PGDG APT packages (preferred) -- Most extensions are installed from the official PostgreSQL APT repository (
apt.postgresql.org). The version you get is whatever the PGDG maintainers have packaged. This is typically the latest stable release, but may lag a few days or weeks behind upstream. -
Source builds (fallback) -- Extensions not available in PGDG are compiled from source at the latest stable release tag.
If the PGDG repository ships an older version than upstream, we ship that older version. This is intentional: PGDG packages are tested against the corresponding PostgreSQL release, receive security patches through the same channel, and are guaranteed to be ABI-compatible. We only override this if there is a critical bug fix or security issue in a newer release that PGDG has not yet packaged.
Why is my extension not included?
Possibly one of:
-
License -- pglayers only ships extensions with permissive open-source licenses (PostgreSQL, MIT, BSD, Apache 2.0, ISC). We exclude proprietary, source-available (BSL, SSPL, FSL, ELv2), and strong copyleft (AGPL) licenses. We also exclude extensions that require proprietary runtime dependencies (e.g., Oracle client). See the Licensing policy section for details.
-
Not yet contributed -- We welcome contributions! To add a new extension, see the Contributing guide for the full checklist, Dockerfile patterns, and test requirements.
Why is there no package for my PostgreSQL version?
Extension availability per PostgreSQL version depends on upstream support:
-
APT-based extensions -- Available when the PGDG repository publishes a package for that PG version. New major PG versions (e.g., PG 19 during beta) may not have all packages yet.
-
Source-built extensions -- Available when the extension compiles cleanly against that PG version's headers.
If an extension you need doesn't support your PG version yet, you can contribute by following the Contributing guide.
Acknowledgements
This project stands on the shoulders of the PostgreSQL community:
- The PostgreSQL Global Development Group for building the best open-source database
- The PGDG APT Repository maintainers who package and distribute extensions for Debian and Ubuntu
- The Official PostgreSQL Docker image maintainers for providing reliable, well-configured base images
- The Debian PostgreSQL team for their packaging work that makes all of this possible
- Every extension author who releases their work under permissive open-source licenses
pglayers is a thin layer of automation on top of their work. Without the quality and consistency of the upstream ecosystem, this project would not exist.