CloudNativePG (CNPG) supports bootstrapping a cluster with initdb, letting you supply initialization options declaratively through a Kubernetes manifest. The BootstrapInitDB configuration accepts custom options alongside the standard cluster spec, so users, roles, databases, encoding and checksum settings all live in version-controlled YAML.

An initdb bootstrap manifest

The following manifest creates a cluster named cluster-example-initdb using the initdb bootstrap method.

apiVersion: v1
data:
  username: Y3liZXJ0ZWM=
  password: Y3liZXJ0ZWMxMjM=
kind: Secret
metadata:
  name: app-secret
type: kubernetes.io/basic-auth
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example-initdb
spec:
  instances: 1
  bootstrap:
    initdb:
      database: cybertec
      owner: cybertec
	  dataChecksums: true
      encoding: 'LATIN1'
	  secret:
        name: app-secret

  managed:
    roles:
      - name: nikola
        login: true
        superuser: false
        createdb: true
      - name: tesla
        superuser: true
        login: true

  storage:
    size: 1Gi

It does four things:

  • creates a Kubernetes secret (app-secret) holding the password for the user cybertec
  • initializes a database named cybertec owned by the user cybertec
  • sets the database encoding to LATIN1 and enables data checksums
  • creates the roles nikola (with createdb) and tesla (as a superuser)

Other available options are documented under bootstrapinitdb. Apply the manifest:

kubectl apply -f single_initdb.yaml

Checking the cluster

Confirm the pod is running:

kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
cluster-example-initdb-1                  1/1     Running   0          2m34s
cnpg-controller-manager-6848689f4-j756l   1/1     Running   0          41h

Then enter the pod to reach PostgreSQL:

kubectl exec -it pod/cluster-example-initdb-1 -- /bin/bash
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
postgres@cluster-example-initdb-1:/$ psql
psql (17.5 (Debian 17.5-1.pgdg110+1))
Type "help" for help.

postgres=# \du
                                     List of roles
   Role name    |                         Attributes                         
----------------+------------------------------------------------------------
 cybertec       | 
 nikola         | Create DB
 postgres       | Superuser, Create role, Create DB, Replication, Bypass RLS
 streaming_replica | Replication
 tesla          | Superuser

postgres=# \l cybertec
                                     List of databases
  Name    |  Owner   | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges 
----------+----------+----------+-----------------+---------+--------+--------+-----------+-------------------
 cybertec | cybertec | LATIN1   | libc            | C       | C      |        |           | 
(1 row)

postgres=# show data_checksums;
 data_checksums 
----------------
 on
(1 row)

Adding roles and databases

Beyond the bootstrap step, roles and databases can be created imperatively through kubectl exec:

kubectl exec -it pod/cluster-example-initdb-1 -- \
  psql -U postgres -c "CREATE ROLE marie LOGIN PASSWORD 'secret';"

kubectl exec -it pod/cluster-example-initdb-1 -- \
  psql -U postgres -c "CREATE DATABASE curie OWNER marie;"

They can also be managed declaratively with the managed and Database custom resources. A separate database.yaml manifest defines a database:

apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
  name: db-one
spec:
  name: one
  owner: app
  cluster:
    name: cybertec-single-instance-example

Apply both manifests:

kubectl apply -f single.yaml
kubectl apply -f database.yaml

This gives you different strategies for managing database infrastructure through IaC. Another manifest shows a further option:

apiVersion: v1
kind: Secret
metadata:
  name: cybertec-secret
type: Opaque
stringData:
  secret.sql: |
    CREATE ROLE cybertec WITH LOGIN PASSWORD 's3cr3t';
    GRANT ALL PRIVILEGES ON DATABASE app TO cybertec;
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-configmap
data:
  configmap.sql: |
    CREATE TABLE IF NOT EXISTS demo (
      id SERIAL PRIMARY KEY,
      name TEXT NOT NULL
    );
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example-initdb
spec:
  instances: 3

  bootstrap:
    initdb:
      database: app
      owner: app
      postInitApplicationSQLRefs:
        secretRefs:
          - name: my-secret
            key: secret.sql
        configMapRefs:
          - name: my-configmap
            key: configmap.sql

  storage:
    size: 1Gi

It creates a secret and a config map that are used after database initialization. Depending on the goal, postInitSQL, postInitApplicationSQL or postInitTemplateSQL can be used to customize the cluster and its databases.

Verifying the declarative objects

kubectl exec -it pod/cluster-example-initdb-1  -- psql -U postgres -d app -c "\du"
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
                                 List of roles
     Role name     |                         Attributes                         
-------------------+------------------------------------------------------------
 app               | 
 cybertec          | 
 postgres          | Superuser, Create role, Create DB, Replication, Bypass RLS

kubectl exec -it pod/cluster-example-initdb-1  -- psql -U postgres -d app -c "\l app"
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
                                          List of databases
 Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges 
------+-------+----------+-----------------+---------+-------+--------+-----------+-------------------
 app  | app   | UTF8     | libc            | C       | C     |        |           | =Tc/app          +
      |       |          |                 |         |       |        |           | app=CTc/app      +
      |       |          |                 |         |       |        |           | cybertec=CTc/app
(1 row)

kubectl exec -it pod/cluster-example-initdb-1  -- psql -U postgres -d app -c "\d"
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
             List of relations
 Schema |    Name     |   Type   |  Owner   
--------+-------------+----------+----------
 public | demo        | table    | postgres
 public | demo_id_seq | sequence | postgres
(2 rows)

The cybertec role exists and has been granted user on the app database, and the demo table was created as expected.

Exposing the cluster outside Kubernetes

Connections from outside the cluster require a custom service configuration. This manifest disables the defaults and declares its own access path:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example-initdb
spec:
  instances: 1
  bootstrap:
    initdb:
      database: cybertec
      owner: cybertec
      secret:
        name: app-secret

  managed:
    roles:
    - name: nikola
      login: true
      superuser: false
      createdb: true
    - name: tesla
      superuser: true
      login: true

    services: ## disable the default services
	  disabledDefaultServices: ["ro", "r"]
      additional:
        - selectorType: rw
          serviceTemplate:
            metadata:
              name: "test-rw"
              labels:
                test-label: "true"
              annotations:
                test-annotation: "true"
            spec:
              type: LoadBalancer
			  ## In order to static IP address
			  ## LoadBalancerIP: Your Static IP              
			  ports:
                - port: 5432
                  targetPort: 5432
                  protocol: TCP
                  name: postgres

  storage:
    size: 1Gi

The configuration exposes the writable pod through a LoadBalancer service named test-rw, and disables the default read-only services (-ro, -r). Without that change, CNPG creates three services by default: rw, ro and r. Apply the cluster configuration:

kubectl apply -f cluster-initdb-lb.yaml

Wait until the pod and service are ready:

kubectl get service
NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)             AGE
cluster-example-initdb-rw    ClusterIP      10.106.204.97   None            5432/TCP            5s
cnpg-webhook-service         ClusterIP      10.98.146.114   None            443/TCP             2d10h
test-rw                      LoadBalancer   10.109.19.23    10.109.19.23    5432:31306/TCP      5s

kubectl describe service test-rw
Name:                     test-rw
Namespace:                cnpg-system
Labels:                   cnpg.io/cluster=cluster-example-initdb
                          cnpg.io/isManaged=true
                          test-label=true
Annotations:              cnpg.io/operatorVersion: 1.26.0
                          cnpg.io/updateStrategy: patch
                          test-annotation: true
Selector:                 cnpg.io/cluster=cluster-example-initdb,cnpg.io/instanceRole=primary
Type:                     LoadBalancer
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.109.19.23
IPs:                      10.109.19.23
LoadBalancer Ingress:     10.109.19.23 (VIP)
Port:                     postgres  5432/TCP
TargetPort:               5432/TCP
NodePort:                 postgres  31306/TCP
Endpoints:                10.244.0.51:5432
Session Affinity:         None
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster
Events:                   None

Inspect the custom service:

kubectl describe service test-rw

The notable value here is LoadBalancer Ingress: 10.109.19.23. Although no static IP was requested, minikube assigned one to the service. That address, together with the target port 5432, is not usable for connecting from outside the cluster; the minikube IP on the local machine is used instead. Retrieve the full connection details:

minikube service test-rw -n cnpg-system --url

A sample output looks like this:

http://192.168.39.229:31306

Connect with psql:

psql -h 192.168.39.229 -U postgres -p 31306

Supply the password when prompted. Once the configuration is correct, the cluster can be queried as usual.

psql (15.13), server 17.5
SSL connection (protocol: TLSv1.3)
Type "help" for help.

Takeaway

CNPG brings infrastructure-as-code discipline to PostgreSQL: bootstrap options, roles and databases are expressed as manifests, which keeps operations consistent and automatable whether the target is Minikube or a full cloud environment.