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>-<env>
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: "7d"
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. On this platform, staging overlays set retentionPolicy: "3d"; prod and Infisical stay 7d. 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>-<env>
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 Deploy & reconcile) and the migrations Job below.

Penvoice applies its schema in-process at startup: no separate Job, no bundled migrate CLI. That’s the simpler default and what to reach for first: fewer moving parts, one less thing that can drift from the app’s actual code.

The alternative. Worth it if you need migrations to strictly complete before any new pod serves traffic. Is a PreSync hook Job, 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>-<env>
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.