Shepherd keeps everything in PostgreSQL — pipelines, organisations, credentials, sessions — and holds no state of its own. That makes the database the one thing worth backing up, and Shepherd itself something you can delete and reinstall freely.
You have two ways to give it one, and both are fully supported.
The default. Point SHEPHERD_DATABASE_URL at an RDS, a Cloud SQL, an Azure
Database, or a Postgres you already run, and put it in the secret the chart reads:
kubectl -n shepherd create secret generic shepherd-secrets \
--from-literal=SHEPHERD_DATABASE_URL='postgres://user:pass@my-rds.eu-west-1.rds.amazonaws.com:5432/shepherd?sslmode=require' \
--from-literal=SHEPHERD_SECURITY_ENCRYPTION_KEY="$(openssl rand -base64 32)"
helm install shepherd oci://ghcr.io/procoduck/charts/shepherd --version 0.22.0 \
--namespace shepherd --set existingSecret=shepherd-secrets
Shepherd needs an ordinary database and an ordinary role that owns it. No superuser, no extensions beyond what a stock PostgreSQL ships, and no manual schema step — migrations run themselves on install and upgrade. Use PostgreSQL 16 or newer: the test suites run 16 and the dev and e2e stacks run 18; older majors are untested.
On EKS or GKE the connection string above does not have to carry a password at all. Both clouds authenticate a pod to a managed database by annotating the ServiceAccount it runs as, and the chart exposes that:
serviceAccount:
annotations:
# EKS, for IAM authentication to RDS
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/shepherd
# or GKE, for Cloud SQL
# iam.gke.io/gcp-service-account: shepherd@project.iam.gserviceaccount.com
By default the chart grants this account no RBAC and mounts no API token — Shepherd
does not call the Kubernetes API — so it exists precisely to be annotated. (Turning on
the receiver tier with receiver.applyTenantRoutes,
its default, adds a Role that writes HTTPRoutes in the release namespace and a ClusterRole
that reads the HTTPRoute CRD, and mounts the token, so Shepherd can apply tenant routes.) Set
serviceAccount.create: false and serviceAccount.name instead to use
an account your platform team already manages; it must exist before you install, because the
migration Job runs as a pre-install hook and a pod naming a missing ServiceAccount is refused.
If you would rather not run a database by hand, CloudNativePG can run it in the same cluster. Install the operator once, then:
helm install shepherd oci://ghcr.io/procoduck/charts/shepherd --version 0.22.0 \
--namespace shepherd \
--set cnpg.enabled=true \
--set existingSecret=shepherd-secrets # still holds the encryption key
The chart renders a postgresql.cnpg.io/v1 Cluster and wires
Shepherd’s database URL to the uri key of the <cluster>-app
Secret the operator generates. You never write a connection string, and
SHEPHERD_DATABASE_URL is not needed in your secret at all.
The chart does not install the operator. CloudNativePG and its CRDs must
already be in the cluster, exactly as with the Gateway API for route.enabled.
Without them the install fails with no matches for kind Cluster.
This path needs Kubernetes 1.29+, which the chart’s own floor of
1.32 already covers: the pinned CloudNativePG operator chart
(0.29.1, see deploy/versions.env) refuses older nodes, and the chart
declares one supported floor for everyone — see
Requirements. A cluster running kindest/node:v1.37.0, the version the
chart’s own CI pins for this exact path, is known good.
pre-install hook, because the migration has nothing to talk to otherwise.
migrations.job.backoffLimit) until it connects. Failed
attempts in the log during a first install are expected, not a fault.
helm uninstall does not delete the database. Hook resources
are not tracked in the release, which for the thing holding all your data is the right way
round. Remove it deliberately:
kubectl -n shepherd delete cluster shepherd-db.
cnpg.* after the first install does nothing. The
chart skips rendering a Cluster that already exists, because a hook Helm re-renders is a
hook Helm first deletes. Edit a live cluster with
kubectl -n shepherd edit cluster shepherd-db, which is the operator’s own
interface for it.
Deploying the chart with Argo CD, Flux, or
helm template | kubectl apply? Set cnpg.render: never once the
database exists. The skip described above uses Helm’s lookup,
which needs a live cluster connection. Tools that render the chart offline and apply the
output have none, so the guard fails open: the Cluster is emitted on every sync, and
Argo CD maps helm.sh/hook: pre-install,pre-upgrade onto a PreSync hook whose
default policy is BeforeHookCreation — delete, then recreate. CloudNativePG
cascades that deletion to the PVCs, the migration Job migrates a fresh empty database, and the
sync reports success. Nothing about the outcome looks like an error.
cnpg.render takes three values. auto (the default) renders only when
lookup says the Cluster is absent — correct under the helm CLI,
unsafe under anything that renders offline. never never emits it, which is what a
GitOps deployment sets after bootstrapping. always emits it unconditionally, for
bootstrapping or for a pipeline that wants the full desired state.
cnpg:
enabled: true
instances: 3 # a primary and two standbys
storage:
size: 20Gi
storageClass: fast-ssd
resources:
requests: { cpu: 500m, memory: 1Gi }
# Anything the chart does not surface goes here verbatim: backups,
# monitoring, affinity, postgresql.parameters.
extraSpec:
backup:
barmanObjectStore:
destinationPath: s3://my-backups/shepherd
Every key is listed in Helm values. Backup, restore and failover are the operator’s job, not Shepherd’s — see the CloudNativePG documentation.
This path is tested against a real operator, not just rendered: Shepherd’s Kubernetes suite installs CloudNativePG into a throwaway cluster on every change to the chart, installs Shepherd with nothing supplied, and asserts the database is provisioned and the migrations actually reached it.