Show HN: Pglayers – PostgreSQL extensions as stackable Docker layers
原文
pglayers
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 53 extensions
docker run -d -e POSTGRES_PASSWORD=secret ghcr.io/pglayers/pglayers-full:17
# Azure Database for PostgreSQL compatible (28 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.
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-postgres
No 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.
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-rc0 | 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 |
| documentdb | 0.113-0 | 17, 18 | MongoDB-compatible document database engine (BSON types and CRUD API) |
| 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.1 | 17, 18, 19 | HTTP client for PostgreSQL (web requests from SQL) |
| hypopg | 1.4.3 | 17, 18, 19 | Hypothetical indexes for what-if analysis |
| ip4r | 2.4.3 | 17, 18, 19 | IPv4/IPv6 range data types with GiST indexing |
| orafce | 4.16.7 | 17, 18, 19 | Oracle compatibility functions and packages |
| 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_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_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_qualstats | 2.1.4 | 17, 18, 19 | Statistics collector for WHERE clause predicates |
| pg_repack | 1.5.3 | 17, 18, 19 | Online table reorganization without heavy locks |
| pg_roaringbitmap | 1.1.0 | 17, 18 | Roaring bitmap data type for fast set operations |
| 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.9 | 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 |
| 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 |
| 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.3 | 17, 18, 19 | Vector similarity search for AI/embeddings |
| plpgsql_check | 2.9.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 |
| 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 |
| tdigest | 1.4.3 | 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.1 | 17, 18 | Time-series hypertables, compression, continuous aggregates |
| 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 |
| 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_net | pg_net |
| pg_partman | pg_partman_bgw |
| pg_qualstats | pg_qualstats |
| pg_squeeze | pg_squeeze |
| pg_stat_monitor | pg_stat_monitor |
| pg_textsearch | pg_textsearch |
| pg_wait_sampling | pg_wait_sampling |
| pgaudit | pgaudit |
| pglogical | pglogical |
| pgsodium | pgsodium |
| pgtt | pgtt |
| plprofiler | plprofiler |
| 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_ALL
By 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.
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;
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 laid out at the correct filesystem paths:
/usr/lib/postgresql/17/lib/vector.so
/usr/share/postgresql/17/extension/vector.control
/usr/share/postgresql/17/extension/vector--0.8.3.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.
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=19beta1
make build-all PG=19 PG_TAG=19beta1
# 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
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
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=myprofile
Published 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:17
If 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:17
Then 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)
├── CONTRIBUTING.md Contribution guide
├── AGENTS.md Agent/CI instructions
├── .github/
│ ├── base-image-digests.json Tracked base image digests
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml Bug report form
│ │ ├── new_extension.yml Extension request form
│ │ └── config.yml Template chooser config
│ └── workflows/
│ ├── build-push.yml CI: builds extensions, pushes to GHCR
│ ├── test.yml CI: full test suite (PG 17, 18, 19)
│ ├── monitor-base-image.yml Detects base image updates (every 6h)
│ ├── monitor-extensions.yml Detects new extension releases (daily)
│ └── cache-cleanup.yml Prunes stale GHA caches
├── extensions/
│ ├── pgvector/
│ │ ├── Dockerfile Multi-stage build -> artifact image
│ │ ├── extension.conf Metadata and version mapping
│ │ └── test.sql Integration tests (PASS/FAIL assertions)
│ ├── pg_cron/
│ ├── postgis/ (complex: bundles runtime libs)
│ ├── ... (53 extensions total)
│ └── 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, BSD, Apache 2.0, ISC) and, where industry practice clearly supports it, GPL-2.0 extensions loaded via PostgreSQL's dynamic extension mechanism.
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.
License
这条对你有帮助吗?