Skip to content

Database & migrations

If <app> needs Postgres, CNPG (the operator installed cluster-wide in the cnpg-system namespace) provisions it from a Cluster CR. CNPG auto-generates an application Secret whose uri key is the connection string — the app and the migrations Job both read DB_URL from it.

flowchart LR
  Cluster[CNPG Cluster<br/>app-pg] -->|auto-creates| Secret[(Secret<br/>app-pg-app)]
  Secret -->|uri → DB_URL| Job[migrations Job<br/>PreSync hook]
  Secret -->|uri → DB_URL| Rollout[Rollout pods]
  Job -->|runs first| Rollout
  Cluster -->|WAL + base backup| OOS[(Object Storage)]

Single instance (one node, no HA). bootstrap.initdb creates the database and an owner role; CNPG then creates the <app>-pg-app secret. The env and backup blocks configure WAL archiving + base backups to Oracle Object Storage (S3-compatible).

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: <app>-pg
namespace: <app>
spec:
instances: 1
# Oracle S3-compat backups: disable AWS SDK checksum trailers (Oracle returns
# NotImplemented) and sign with the bucket's real region.
env:
- name: AWS_REQUEST_CHECKSUM_CALCULATION
value: when_required
- name: AWS_RESPONSE_CHECKSUM_VALIDATION
value: when_required
- name: AWS_DEFAULT_REGION
value: <region>
storage:
size: 5Gi
storageClass: local-path # k3s default (node-local disk)
bootstrap:
initdb:
database: <app>
owner: <app>
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
# Continuous backup + WAL archiving to Object Storage; creds in Secret
# <app>-pg-backup-creds (created out of band — see Secrets).
backup:
retentionPolicy: "30d"
barmanObjectStore:
destinationPath: "s3://<bucket>/"
endpointURL: "https://<os-namespace>.compat.objectstorage.<region>.oraclecloud.com"
s3Credentials:
accessKeyId:
name: <app>-pg-backup-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: <app>-pg-backup-creds
key: ACCESS_SECRET_KEY
wal:
compression: gzip
data:
compression: gzip

The <app>-pg-backup-creds secret is created by hand (Oracle Customer Secret Key) — see Secrets. barmanObjectStore is deprecated upstream (removed in CNPG 1.30); migrate to the Barman Cloud plugin later.

Pair the cluster with a ScheduledBackup so WAL archiving + a nightly base gives PITR across the retention window. CNPG cron is 6-field (seconds first).

apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: <app>-pg-daily
namespace: <app>
spec:
schedule: "0 0 2 * * *" # 02:00 daily
backupOwnerReference: self
cluster:
name: <app>-pg

Reading DB_URL from the auto-generated secret

Section titled “Reading DB_URL from the auto-generated secret”

CNPG creates Secret/<app>-pg-app automatically. Its uri key holds a ready-to-use connection string — wire it straight into DB_URL:

env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: <app>-pg-app # auto-created by CNPG from the Cluster name
key: uri

You never write this secret yourself, and it never goes in Git. The same secretKeyRef is used in both the Rollout (from GitOps & deploy) and the migrations Job below.

Run schema migrations before new pods start, using the migrate CLI bundled into the image (from Build & CI). The Argo CD PreSync hook ordering guarantees migrations complete before the Rollout updates; hook-delete-policy: BeforeHookCreation cleans up the previous run.

apiVersion: batch/v1
kind: Job
metadata:
name: <app>-migrate
namespace: <app>
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 3
activeDeadlineSeconds: 300
template:
metadata:
labels:
app: <app>-migrate
spec:
restartPolicy: Never
containers:
- name: migrate
image: ghcr.io/<owner>/<app>:sha-XXXXXXX
command: ["/app/migrate"]
args: ["-path", "/app/migrations", "-database", "$(DB_URL)", "up"]
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: <app>-pg-app
key: uri

Keep the Job’s image tag in lockstep with the Rollout’s tag so migrations and code ship together.

Canary & metrics — expose /metrics and gate the canary on the success rate.