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: 16 produces 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: 17 triggers an offline in-place pg_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

  1. Pin SHA256 digests, never tags. Mutable tags such as :16 or :latest inside a catalog undermine byte-for-byte reproducibility and leave you exposed to overwritten or compromised registry tags.
  2. 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: unsupervised deliberately, so restarts never collide with peak traffic.