Running production databases on Kubernetes means someone has to answer:
- How do I back up this database automatically?
- Where do the backups go?
- How do I restore into another cluster or instance?
- How do I do this for multiple database types without writing a different tool for each one?
kube-db-backup solves this by introducing a Kubernetes-native operator with a pluggable engine model. The operator handles scheduling, lifecycle, retries, S3 storage, and observability. Small, swappable engine container images know only how to dump/restore one specific database type. Today it supports PostgreSQL; tomorrow it can support MongoDB, MySQL, Milvus, Weaviate, or anything else by adding a new engine image — without changing the operator.
The core design principle is separation of concerns: the generic lifecycle belongs to the operator; the database-specific mechanics belong to the engine.
- A user creates
DatabaseInstance,BackupConfig, and either aBackup(one-shot) orBackupSchedule(cron) resource. - The operator validates the references, looks up the engine type in the Engine Registry, and creates a Kubernetes Job.
- The Job runs an engine container image (e.g.,
pg-engine) with a standardized set of environment variables and mounted secrets. - The engine connects to the database, runs
pg_dump/pg_restore, uploads/downloads from S3, and writes a standardized JSON result. - The operator reads the Job status and result, then updates the
Backup/RestoreCRD status.
All CRDs live in api/v1alpha1/ and are database-agnostic. They describe the backup lifecycle, not Postgres specifically.
- Purpose: describes how to connect to a database.
- Key fields:
engineType(e.g.,postgres) — tells the operator which engine image to use.host,port,databaseName— connection details.authSecretRef— reference to a Secret containingusernameandpassword.engineConfig— free-form key/value map for engine-specific options (e.g.,dumpFormat: custom,sslMode: disable).
- Why it matters: the same CRD works for Postgres, MongoDB, or any future engine. Adding a new engine does not require changing this CRD.
- Purpose: describes where backups go in S3 and how long to keep them.
- Key fields:
s3Endpoint,s3Bucket,s3Prefix,s3Region,s3ForcePathStyle— S3 destination.s3SecretRef— reference to a Secret containingaccess-keyandsecret-key.retentionPolicy—maxBackupsandmaxAgeDays(planned lifecycle automation).activeDeadlineSeconds— maximum runtime for a backup Job.
- Why it matters: storage policy is completely decoupled from the database being backed up.
- Purpose: one-shot backup request.
- Key fields:
databaseInstanceRef— what to back up.backupConfigRef— where to put it.
- Status fields:
phase(Pending/Running/Succeeded/Failed),sizeBytes,checksumSha256,s3Objects,tablesCount,retryCount,jobName. - Why it matters: it is the unit of work. It maps 1:1 to a Kubernetes Job.
- Purpose: cron-based generator of
Backupobjects. - Key fields:
schedule— cron expression.databaseInstanceRefandbackupConfigRef.suspend— pause without deleting.successfulJobsHistoryLimit/failedJobsHistoryLimit— how many oldBackupobjects to keep.
- Why it matters: scheduling is engine-agnostic. It behaves like Kubernetes
CronJob→Job, but for backups.
- Purpose: restore a backup into a target database instance.
- Key fields:
backupRef— restore from an existingBackupobject.s3Path— alternative: restore from a raw S3 key.targetInstanceRef— where to restore.restoreClean— drop existing objects before restoring.force— allow restoring into a non-empty database.
- Why it matters: restores are explicit, auditable, and validated (engine type must match between source and target).
Located in pkg/engine/registry.go.
- A static map from
engineTypetoEngineInfo. - Each entry defines the container image and default port for that engine.
- The registry can be overridden at runtime via environment variables (e.g.,
PG_ENGINE_IMAGE). - Why it matters: this is the only place the operator knows about Postgres. To add MongoDB, you add one entry and ship a new image — no controller changes.
Located in pkg/engine/contract.go.
The contract defines exactly how the operator and an engine image communicate:
- Input: standardized environment variables (
OPERATION,DB_HOST,DB_PORT,DB_NAME,S3_ENDPOINT,S3_BUCKET, etc.). - Secrets mounted as files:
/etc/db-secret/password/etc/s3-secret/access-key/etc/s3-secret/secret-key
- Exit codes:
0success1engine internal error2auth failure3connectivity failure — retried4storage failure — retried
- Output: a JSON result wrapped in
___RESULT_START___/___RESULT_END___markers, written to stdout.
The contract also defines retry behavior: max 3 retries with backoff delays of 30s, 2m, 5m.
Located in internal/controller/.
- Watches
Backupobjects. - Validates
DatabaseInstanceandBackupConfigreferences. - Looks up the engine image from the registry.
- Creates a Kubernetes Job with the engine contract inputs.
- Polls the Job status and maps the engine result back to
Backupstatus. - Implements retry logic for connectivity/storage failures.
- Watches
BackupScheduleobjects. - Computes the next run time from the cron expression.
- Creates
Backupobjects when the schedule fires. - Prunes old
Backupobjects according to history limits.
- Watches
Restoreobjects. - Validates the target
DatabaseInstanceandBackupConfig. - Validates that the source backup's engine type matches the target instance's engine type.
- Creates a Job to run the restore engine.
- Polls the Job and updates
Restorestatus.
Located in engines/postgres/.
An engine image is a standalone container with native database tooling.
Dockerfile— based onpostgres:16-alpine, includesaws-cli.entrypoint.sh— dispatches tobackup.shorrestore.shbased onOPERATION.backup.sh— runspg_dump -Fc, writes abackup-manifest.json, and uploads both files to S3.restore.sh— downloads the manifest and dump from S3, validates target DB emptiness unlessFORCE=true, runspg_restore --clean, and runsANALYZE.
Why this design is powerful: each engine is a black box. It can use completely different tooling (pg_dump, mongodump, pgBackRest, Milvus's backup API) without the operator needing to understand any of it.
Backups are organized as:
s3://{bucket}/{prefix}/backups/{engineType}/{databaseName}/{backupId}/
├── dump.{format}
└── backup-manifest.json
The manifest is self-describing and includes checksum, timestamp, size, and S3 object keys. This means even if the Kubernetes Backup CRD is deleted, the S3 objects remain discoverable and restorable.
Located in charts/kube-db-backup/.
The chart packages everything:
crds/— all 5 CRDs (Helm installs these first).templates/— operator Deployment, RBAC, Secrets, and application CRs (DatabaseInstance,BackupConfig,BackupSchedule).values.yaml— single file where users configure DB connections, S3 credentials, schedules, and engine/operator images.
The operator Deployment receives PG_ENGINE_IMAGE as an environment variable so the chart controls which engine image the operator dispatches.
docker-compose.yml— spins up Postgres 16 and MinIO for local testing.Makefile— build, test, docker-build targets.
| Concern | Handled by |
|---|---|
| Scheduling | BackupSchedule + BackupScheduleReconciler |
| Job lifecycle / retries | Controllers |
| Storage target / retention | BackupConfig |
| Database connection | DatabaseInstance |
| Actual dump/restore | Engine image |
| S3 upload/download | Engine image |
| Result contract | pkg/engine/contract.go |
| Multi-engine dispatch | Engine Registry |
| Packaging / install | Helm chart |
To add a new database engine:
- Build a new engine image that follows the contract.
- Add its entry to the Engine Registry (or later, a
BackupEngineCRD). - Done. No CRD changes, no controller changes.
This is the project's key success criterion: the abstraction is validated when adding a new engine requires only a new image and a registry entry.