The Immutable Future of PostgreSQL Extensions in Kubernetes with CloudNativePG

Table of Contents
Managing extensions is one of the biggest challenges in running PostgreSQL on
Kubernetes. In this article, I explain why I believe
CloudNativePG
—now a CNCF Sandbox project—is on the verge of a breakthrough.
Two important new features for both PostgreSQL and Kubernetes—the
extension_control_path
option and image volumes—will guarantee immutability
to extension container images.
A core principle of Kubernetes is the immutability of container images. With CloudNativePG, we embraced this approach from day one, firmly adhering to best practices such as immutable infrastructure, Infrastructure as Code (IaC), and shifting security left.
This immutability means that PostgreSQL container images must remain read-only to ensure security and reliable change management. However, this approach clashes with one of PostgreSQL’s greatest strengths: extensibility. Extensions have indeed played a crucial role in PostgreSQL’s rise as the most popular and versatile database engine.
While core extensions like pg_stat_statements
and postgres_fdw
are
included in every
PostgreSQL operand image,
third-party extensions—such as PostGIS, TimescaleDB, and pgvector—are separate
projects and are not bundled with PostgreSQL by default.
In traditional virtual machine (VM) deployments, extensions can be installed at runtime using the package manager of the Linux distribution. However, with CloudNativePG, modifying the container image is not possible; you must rely on declarative configuration and pre-existing images. Additionally, deployment is only part of the challenge: updates as part of Day 2 operations are usually handled case-by-case, impacting scalability and maintainability of infrastructures and applications sensibly.
The Current Approach: Pre-built Operand Images #
Until now, the only way to run third-party extensions with CloudNativePG (and most PostgreSQL operators) without breaking immutability has been to embed them in the operand image. However, this approach has significant downsides:
- Increased image size and footprint – The more extensions included, the larger the image.
- Rigid extension selection – Users are often left frustrated because:
- Their required extension isn’t included.
- They want a lighter image without unnecessary extensions.
- They don’t want specific libraries due to CVEs or security concerns.
- Operational complexity – Users must maintain custom images, as outlined in our guide on creating container images and following our image requirements.
Not everyone has the resources or expertise to do this. Compliance is another critical factor.
We have long been exploring ways to enable dynamic loading of extensions in CloudNativePG. I recall insightful discussions with Tembo, an early adopter of CloudNativePG, about reserving a volume for dynamically installing third-party extensions. However, we decided not to pursue that idea, as it violated our immutability principle (Tembo later pursued their Trunk project).
To achieve dynamic extension loading without breaking immutability, we needed improvements in both PostgreSQL and Kubernetes.
Fortunately, these improvements are finally happening.
What’s Missing in PostgreSQL? #
Currently, PostgreSQL requires extensions to be installed in a system
directory, such as /usr/share/postgresql/17/extension
, where it looks for
extension control files.
In traditional setups, Linux package managers (Debian/RPM) install extension packages, placing control files in this directory. While this works in a mutable environment, it is incompatible with an immutable setup like CloudNativePG, where the system directory is read-only for security.
To overcome this limitation, PostgreSQL needs to support alternative extension locations. This is precisely what the patch developed by my EDB colleagues Peter Eisentraut, Andrew Dunstan, and Matheus Alcantara and proposed for PostgreSQL 18 aims to do. The patch is based on an initial work by Christoph Berg. It is part of a broader discussion started by David Wheeler (Tembo), which I contributed to to align it with Kubernetes’ immutability needs. We further refined this direction during in-person conversations at PostgreSQL Europe in Athens in October 2024 ( David wrote a great recap here).
Introducing extension_control_path
#
The proposed patch introduces a new PostgreSQL configuration option (GUC),
extension_control_path
, which allows users to specify additional
directories for extension control files. It is set to $system
by default,
but users can define multiple paths—just like other path-like configuration
options in PostgreSQL.
Combined with dynamic_library_path
, this feature enables PostgreSQL to
locate control files and shared libraries from multiple directories, breaking
free from the single system-wide location constraint.
I hope this patch is merged into PostgreSQL and becomes part of PostgreSQL 18, ensuring that future versions fully support this approach.
What’s Missing in Kubernetes? #
Even with PostgreSQL supporting multiple extension paths, we still need a way to dynamically mount extensions into a running CloudNativePG cluster without modifying the primary container image. This is where Kubernetes is stepping up.
Introducing ImageVolume
resources #
The ImageVolume feature was introduced as an alpha feature in Kubernetes 1.31 and is expected to reach beta soon. Its goal is to be promoted to a beta release state in Kubernetes 1.33.
This feature allows us to mount a container image as a read-only and
immutable volume inside a running pod. It will enable PostgreSQL extensions
that are packaged as independent OCI-compliant container images to be
mounted inside CloudNativePG clusters at a known directory (e.g.,
/extensions/<EXTENSION_NAME>
).
At this point, all we’ll need to do is set the extension_control_path
and
dynamic_library_path
options accordingly. An operator like CloudNativePG
can automate this process, making it seamless for users. The same approach can
be repeated multiple times, once per required extension.
Image extension images at that point must be compatible with the Postgres base image that the CloudNativePG has deployed (for example, the same distribution and architecture).
How We Tested These Features in CloudNativePG #
Although we weren’t directly involved in developing these new PostgreSQL features, some of us at CloudNativePG contributed to testing the corresponding patch.
I want to give special thanks to Niccolò Fei for his outstanding work validating the patch developed by Peter, Andrew, and Matheus. This includes patches for the CloudNativePG operator and extension images.
Our testing focused on:
- Streamlining the build process for PostgreSQL operand images that incorporate patches from the PostgreSQL commit fest (for details, see my previous article).
- Developing a pilot patch for CloudNativePG to declaratively add
PostgreSQL extensions via a container image. The operator mounts the image as
an
ImageVolume
, automatically configuringextension_control_path
anddynamic_library_path
. See CloudNativePG PR #6546. - Creating self-contained extension images, such as one for
pgvector
.
Lightweight Extension Images #
Focusing on the last point, Niccolò proposed a
Dockerfile for pgvector
that produces a minimal image—containing only the lib
and share
directories—at just 1.6MB.
To inspect its contents, use:
dive ghcr.io/cloudnative-pg/pgvector-18-testing:latest
Deploying Extensions Declaratively #
To illustrate the proposed solution, consider this example (note: the format is still in alpha and may change):
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-with-extensions
spec:
instances: 1
# This points to the temporary build of the commit fest patch
imageName: ghcr.io/cloudnative-pg/postgresql-trunk:18-cf-4913
postgresql:
extensions:
- name: pgvector
image:
reference: ghcr.io/cloudnative-pg/pgvector-18-testing:latest
storage:
storageClass: standard
size: 1Gi
The key addition is the .spec.postgresql.extensions
section, where users can
define extensions and their corresponding images (each an ImageVolume
resource). This configuration can be updated dynamically.
What Happens Under the Hood #
When an extension image is specified:
The operator triggers a rolling update, starting with replicas.
Each referenced image is mounted as a read-only volume using Kubernetes'
ImageVolume
resource.In this example,
ghcr.io/cloudnative-pg/pgvector-18-testing:latest
is mounted on/extensions/pgvector
.CloudNativePG updates
dynamic_library_path
andextension_control_path
to include the/extensions/pgvector/lib
and/extensions/pgvector/share
directories, respectively.
Once deployed, you can verify the extension’s availability:
postgres=# SELECT * FROM pg_available_extensions WHERE name = 'vector';
-[ RECORD 1 ]-----+-----------------------------------------------------
name | vector
default_version | 0.8.0
installed_version |
comment | vector data type and ivfflat and hnsw access methods
Important: This feature is not yet available out of the box. To use ImageVolume
, you need:
- Kubernetes 1.31+ with the ImageVolume feature gate enabled.
- CRI-O as the container runtime (containerd support has been merged but is not yet available).
We applied the same approach to PostGIS during our pilot project, creating a self-contained extension image. The method proved effective even for complex extensions.
Conclusion #
This is just the beginning. This initiative marks the first iteration toward a new approach to managing PostgreSQL extensions in Kubernetes—bridging the gap between extension developers and consumers. The most important takeaway is that we’re heading in the right direction, opening the door to new possibilities and allowing the best solutions to emerge through exploration.
While PostgreSQL’s extension_control_path
and Kubernetes'
ImageVolume
feature are fundamental pieces, there’s another
critical component: the distribution of PostgreSQL extensions.
It’s time for PostgreSQL extension developers to embrace OCI images as
first-class artifacts, alongside traditional RPM and Debian packages.
Ideally, every extension should provide a self-contained image for mainstream
Linux distributions. Whether these images are built from existing packages or
directly from source (e.g., via Makefile
) is a decision best left to those
with deeper expertise in the packaging ecosystem.
With PostgreSQL 18 supporting configurable extension paths and Kubernetes
1.33 introducing ImageVolume
, CloudNativePG is entering a new era of
dynamic, immutable, and scalable extension management. By packaging
extensions as independent OCI-compliant images, we can finally decouple
PostgreSQL operand images from extensions, keeping them minimal and flexible.
This unlocks several key benefits:
- Install third-party extensions dynamically—no need to rebuild container images.
- Facilitate testing and validation of extensions
- Simplify PostgreSQL extension upgrades without affecting the core database image.
- Ensure strict immutability while enhancing security, change management, scalability, and maintainability.
This is a game-changer for CloudNativePG and the broader PostgreSQL-on-Kubernetes ecosystem.
The future of PostgreSQL extensions in Kubernetes is immutable, yet flexible—just as it should be.
Stay tuned for the upcoming recipes! For the latest updates, consider subscribing to my LinkedIn and Twitter channels.
If you found this article informative, feel free to share it within your network on social media using the provided links below. Your support is immensely appreciated!
Cover Picture: “Sofyan Efendi Dipinggiran Sungai“.