Skip to main content

Database Backup and Recovery

This document describes the database backup and recovery strategy for Open ACE, covering both Kubernetes deployments (k8s/extras/backup/) and Docker Compose deployments (docker-compose.yml).

Issue #1853: Kubernetes database backup reference implementation

Overview​

Open ACE provides optional database backup infrastructure:

  • Scheduled PostgreSQL backups via CronJob (Kubernetes)
  • Manual pg_dump/pg_restore procedures (Docker Compose)
  • Integrity verification using pg_restore --list
  • Multiple object storage backend support (AWS S3, MinIO, etc.)
  • Restore Job for disaster recovery (Kubernetes)

Deployment options​

OptionEnvironmentBackup ownerTypical RPO
Managed PostgreSQL (RDS/Aurora, Cloud SQL, Azure DB, Alibaba RDS)Production with SLACloud provider (automated backups, PITR)< 5 minutes
Self-managed CronJob (k8s/extras/backup/)Kubernetes, cost-sensitiveThis repo's manifests6–24 hours (CronJob frequency)
Manual pg_dump on the postgres-data volumeDocker Compose single hostYou, via this guideWhatever you schedule

Kubernetes backup (self-managed)​

Read this first: the object storage Secret is NOT part of the kustomization​

k8s/extras/backup/kustomization.yaml lists exactly these resources:

  • serviceaccount.yaml
  • rbac.yaml
  • backup-script-configmap.yaml
  • cronjob.yaml
  • restore-job.yaml

The credentials file (secret-s3.yaml, created from secret-s3.yaml.example, or secret-minio.yaml from secret-minio.yaml.example) is not among them. If you follow the classic "copy the example, then kubectl apply -k ." recipe, the apply succeeds, the CronJob is created, and every backup job then fails at the upload step because the backup-storage-credentials Secret referenced by the CronJob does not exist — the failure is silent unless you check job logs.

Apply the Secret explicitly, then the kustomization:

cd k8s/extras/backup/

# 1. Create and edit credentials
cp secret-s3.yaml.example secret-s3.yaml
# Edit secret-s3.yaml with your S3 credentials

# 2. Apply the Secret explicitly (it is NOT included by kustomization.yaml)
kubectl apply -f secret-s3.yaml

# 3. Apply the rest of the backup manifests
kubectl apply -k .

# 4. Verify CronJob AND Secret
kubectl get cronjob -n open-ace
kubectl get secret backup-storage-credentials -n open-ace

Trigger a manual backup (testing)​

# Create a one-time job from the CronJob
kubectl create job --from=cronjob/postgres-backup manual-backup-$(date +%Y%m%d) -n open-ace

# Watch logs
kubectl logs -f job/manual-backup-$(date +%Y%m%d) -n open-ace

CronJob parameters​

Values below are what k8s/extras/backup/cronjob.yaml actually ships:

ParameterValueNotes
Schedule0 2 * * *Daily at 02:00 UTC (cronjob.yaml:18)
activeDeadlineSeconds36001 hour timeout; increase to 7200 for large databases (cronjob.yaml:33)
CPU request / limit200m / 500m(cronjob.yaml:132-137)
Memory request / limit256Mi / 512MiIncrease to 1Gi for databases > 1GB (cronjob.yaml:132-137)
concurrencyPolicyForbidNo overlapping runs
backoffLimit2Retry up to 2 times on failure
successfulJobsHistoryLimit / failedJobsHistoryLimit3 / 3Kept for debugging
Imagesalpine:3.19 (init: installs AWS CLI into an emptyDir) + postgres:15-alpine (main)postgres:15-alpine does not include AWS CLI

Backup content​

ComponentMethodFrequencyRetention
PostgreSQL databasepg_dump -Fc (custom format)Daily30 days
PostgreSQL globalspg_dumpall --globals-onlyDaily30 days
Redis (optional, BACKUP_REDIS)RDB snapshotDaily7 days

Backup integrity verification​

Each backup includes automatic integrity verification:

  1. After backup creation: pg_restore --list db.dump validates the file is parseable
  2. If verification fails: Job exits with code 1, triggering CronJob retry
  3. Success indicator: Job exits with code 0, backup uploaded to object storage
# Manual verification
pg_restore --list backup.dump > /dev/null && echo "Valid" || echo "Corrupted"

Object storage credentials​

AWS S3 (secret-s3.yaml):

# secret-s3.yaml
apiVersion: v1
kind: Secret
metadata:
name: backup-storage-credentials
namespace: open-ace
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
AWS_REGION: "us-east-1"
S3_BUCKET: "my-open-ace-backups"
S3_ENDPOINT: "" # Leave empty for AWS S3

MinIO (secret-minio.yaml, from secret-minio.yaml.example):

# secret-minio.yaml
apiVersion: v1
kind: Secret
metadata:
name: backup-storage-credentials
namespace: open-ace
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "minioadmin"
AWS_SECRET_ACCESS_KEY: "minioadmin"
AWS_REGION: "us-east-1"
S3_BUCKET: "open-ace-backups"
S3_ENDPOINT: "http://minio.example.com:9000"

Recovery procedure (Kubernetes)​

# Step 1: Identify the backup in your object storage
aws s3 ls s3://your-bucket/open-ace/

# Step 2: Download it
aws s3 cp s3://your-bucket/open-ace/20240115/backup.tar.gz .
tar -xzf backup.tar.gz

# Step 3: Verify integrity
pg_restore --list db.dump

# Step 4: Stop application writers, then apply the restore Job
kubectl scale deployment open-ace --replicas=0 -n open-ace
export RESTORE_TIMESTAMP=20240115
kubectl apply -f restore-job.yaml
kubectl logs -f job/postgres-restore -n open-ace

# Step 5: Restart and verify the application
kubectl scale deployment open-ace --replicas=3 -n open-ace
kubectl exec -it deployment/open-ace -n open-ace -- \
curl -s http://localhost:19888/health

The backup Job is compatible with the existing NetworkPolicy (PostgreSQL 5432, Redis 6379, and external HTTPS 443 egress are already allowed); no NetworkPolicy modification is required.

Monitoring and troubleshooting​

# List recent backup jobs
kubectl get jobs -n open-ace -l app.kubernetes.io/component=backup

# Check last job status
kubectl describe job -n open-ace -l app.kubernetes.io/component=backup

# View job logs
kubectl logs job/postgres-backup-$(date +%Y%m%d) -n open-ace

Common failures:

SymptomCauseFix
Job runs, dump succeeds, upload fails with missing credentialssecret-s3.yaml was never applied (it is not in the kustomization)kubectl apply -f secret-s3.yaml
Job OOM-killed on large databases512Mi memory limit too lowPatch the CronJob memory limit to 1Gi
CronJob never startsSuspended, or previous job still within the 1h deadlineCheck suspend flag and activeDeadlineSeconds

Docker Compose backup and restore​

The Compose stack runs PostgreSQL 15 (postgres:15-alpine) with all data on the named volume postgres-data, mounted at /var/lib/postgresql/data. Defaults come from the environment: user ${DB_USER:-ace}, database ${DB_NAME:-ace}. Back up with pg_dump; never copy the volume directory while PostgreSQL is running.

Backup​

# Custom-format dump streamed out of the container (-T disables TTY for redirection)
mkdir -p ./backups
docker compose exec -T postgres pg_dump -U ${DB_USER:-ace} -d ${DB_NAME:-ace} -Fc \
> ./backups/ace-$(date +%Y%m%d-%H%M).dump

# Verify it is parseable
pg_restore --list ./backups/ace-$(date +%Y%m%d-%H%M).dump > /dev/null && echo "Valid"

Plain SQL dump (greppable, larger):

docker compose exec -T postgres pg_dump -U ${DB_USER:-ace} -d ${DB_NAME:-ace} \
> ./backups/ace-$(date +%Y%m%d-%H%M).sql

Restore​

# 1. Stop the writers (app and, if enabled, scheduler)
docker compose stop app scheduler

# 2. Restore over the existing database (custom format only)
cat ./backups/ace-20260929-0200.dump \
| docker compose exec -T postgres pg_restore -U ${DB_USER:-ace} -d ${DB_NAME:-ace} --clean --if-exists

# 3. Restart and verify
docker compose start app scheduler
docker compose exec -T postgres psql -U ${DB_USER:-ace} -d ${DB_NAME:-ace} -c "SELECT version();"
curl http://localhost:19888/health

Restoring to a fresh volume​

If the postgres-data volume was lost entirely:

docker compose down
docker volume rm open-ace_postgres-data # adjust the compose project prefix if needed
docker compose up -d postgres # empty database re-created from env vars
cat ./backups/ace-20260929-0200.dump \
| docker compose exec -T postgres pg_restore -U ${DB_USER:-ace} -d ${DB_NAME:-ace}
docker compose up -d

After any restore, run alembic upgrade head if the backup predates the current schema (see ../dev/SCHEMA_MIGRATION_GUIDE.md).

Scheduling (optional)​

Cron on the Docker host:

0 2 * * * cd /path/to/open-ace && docker compose exec -T postgres pg_dump -U ace -d ace -Fc > backups/ace-$(date +\%Y\%m\%d).dump

Multi-tenant note​

pg_dump captures all tenants automatically — no special backup configuration is needed. After a restore, verify that all tenants are present and run the application-level permission validation.

Recovery drill checklist​

  1. Download the latest backup from object storage (or ./backups/)
  2. Verify integrity (pg_restore --list)
  3. Restore to a test database
  4. Verify row counts and schema validation
  5. Start the application against the restored database and run smoke tests
  6. Document the RTO achieved