Why per-cluster image tags don't scale
In a conventional Kubernetes operator deployment, each Cluster custom resource carries its own container image tag or SHA256 digest. That works until the fleet reaches dozens or hundreds of PostgreSQL instances spanning several environments. At that point, three problems dominate:
- Configuration drift and toil. A minor PostgreSQL release such as 16.14 to 16.15 means editing every Cluster manifest across repositories and GitOps pipelines.
- Supply chain exposure. Individual teams choosing arbitrary or mutable tags (:16, :latest) routes unvetted images into production, outside security controls.
- Extension distribution. Shipping extensions such as pgvector or PostGIS across multiple PostgreSQL major versions normally forces custom container images or bespoke builds per cluster.
Catalog scopes and what they decouple
CloudNativePG's ImageCatalog (namespaced) and ClusterImageCatalog (cluster-scoped) separate the database lifecycle from the cluster definition. A DBA publishes one central catalog of approved, SHA256-pinned images and extension definitions keyed by PostgreSQL major version. Applications request only major: 16 or major: 17. When the catalog's image digest changes, the operator rolls controlled updates across every cluster that references it.
The scope decision matters: ClusterImageCatalog is for organization-wide baseline images governed centrally, while ImageCatalog suits a team needing custom PostgreSQL images — private C extensions, experimental builds — confined to its own namespace.
Note that the operator does not verify a declared major number against the container binaries at runtime; it trusts the catalog. Your CI/CD pipeline has to confirm that an image listed under major: 16 really runs 16.x, otherwise clusters can loop on boot.
Minor versus major upgrades
The two cases behave very differently.
- Minor (16.4 → 16.5). Changing the SHA digest under
major: 16produces an online, zero-downtime rolling update. The data directory is left alone because minor releases share the same disk format. - Major (16 → 17). Pointing a cluster at
major: 17triggers an offline in-placepg_upgrade. The cluster is shut down while files on disk are converted to the new major format, then replicas are re-cloned. Schedule a maintenance window for these jumps.
Configuring a catalog and pointing clusters at it
A cluster-scoped catalog maps majors 16, 17 and 18 to approved, digest-pinned images:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# catalog-enterprise.yaml apiVersion: postgresql.cnpg.io/v1 kind: ClusterImageCatalog metadata: name: postgresql-enterprise spec: images: - major: 16 image: ghcr.io/cloudnative-pg/postgresql:16.15-202609210816-minimal-trixie@sha256:0b3082c00f138c3d859bf495bccf77785a61ca03f876a72ca6826a6d9df5f528 - major: 17 image: ghcr.io/cloudnative-pg/postgresql:17.11-202609210816-minimal-trixie@sha256:cfc42005357630d2c0bfca095c93a6abe44884486acb0de2e5e511a087b7eb15 - major: 18 image: ghcr.io/cloudnative-pg/postgresql:18.6-202609210816-minimal-trixie@sha256:0fae0cb527912426a889ec0b21b9733e5b76fdc6ad8909914a6e8a6c8ba4f64b |
Apply it:
|
1 |
kubectl apply -f catalog-enterprise.yaml |
Application clusters then reference the catalog and pick a major. spec.imageName is omitted entirely:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# app-db-cluster.yaml apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: payment-service-db namespace: production spec: instances: 3 imageCatalogRef: apiGroup: postgresql.cnpg.io kind: ClusterImageCatalog name: postgresql-enterprise major: 16 storage: size: 5Gi |
Deploy the database:
|
1 |
kubectl apply -f app-db-cluster.yaml |
Shipping a patch without touching application manifests
When a patch lands — 16.15 to 16.16, say — the only edit is the catalog file:
|
1 2 3 4 5 |
# Update the SHA256 digest or tag for major: 16 in catalog-enterprise.yaml spec: images: - major: 16 image: ghcr.io/cloudnative-pg/postgresql:16.16@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
Apply the updated catalog:
|
1 |
kubectl apply -f catalog-enterprise.yaml |
The operator notices the new image SHA for postgresql-enterprise and begins a controlled rolling update of every Cluster referencing it at major: 16. Replicas move first, one at a time; once they are healthy, a graceful switchover hands over the primary, keeping service disruption minimal.
Operating notes
- Pin SHA256 digests, never tags. Mutable tags such as
:16or:latestinside a catalog undermine byte-for-byte reproducibility and leave you exposed to overwritten or compromised registry tags. - Treat a catalog change as a fleet-wide rollout. Because the update immediately triggers rolling upgrades everywhere, align catalog edits with CloudNativePG maintenance window settings, or set
primaryUpdateStrategy: unsuperviseddeliberately, so restarts never collide with peak traffic.



