How‑To: Nightly Postgres → MinIO Backups with Kubernetes CronJob (plus lifecycle)


Updated: 2025-08-24

Overview

Create a CronJob that runs `pg_dumpall`, compresses the dump, and uploads to MinIO. Add a lifecycle rule to age off old backups automatically.

Prerequisites

- Postgres reachable from the cluster
- MinIO endpoint and access keys
- kubectl access to create Secrets and CronJobs

Secrets (example):

kubectl -n backups create secret generic minio-env   --from-literal=MC_HOST_minio="http://ACCESSKEY:SECRETKEY@minio.minio.svc.cluster.local:9000"   --from-literal=BUCKET="pg-backups"

kubectl -n backups create secret generic pg-env   --from-literal=PGHOST=postgres.postgres.svc.cluster.local   --from-literal=PGPORT=5432   --from-literal=PGUSER=postgres

CronJob manifest:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: pg-backup
  namespace: backups
spec:
  schedule: "0 3 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 2
  jobTemplate:
    spec:
      backoffLimit: 0
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: backup
            image: alpine:3.20
            envFrom:
            - secretRef: { name: minio-env }
            - secretRef: { name: pg-env }
            command: ["/bin/sh","-lc"]
            args:
            - |
              set -euo pipefail
              apk add --no-cache postgresql-client minio-client
              ts=$(date -u +%Y%m%dT%H%M%SZ)
              pg_dumpall -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -f /tmp/cluster.sql
              gzip -9 /tmp/cluster.sql
              mc mb -p minio/"$BUCKET" || true
              mc cp /tmp/cluster.sql.gz minio/"$BUCKET"/cluster-$ts.sql.gz
            resources:
              requests: { cpu: "50m", memory: "128Mi", ephemeral-storage: "1Gi" }
              limits:   { cpu: "500m", memory: "512Mi", ephemeral-storage: "5Gi" }

Lifecycle policy (delete after 14 days):

# one-off job/pod to set ILM on the bucket
kubectl -n backups run --rm -it mc --image=alpine:3.20 -- sh -lc '
  apk add --no-cache minio-client &&
  mc alias set minio "$MC_HOST_minio" &&
  mc ilm add --expiry-days 14 minio/pg-backups &&
  mc ilm ls minio/pg-backups'

Restore (Quick):

# Download a backup and restore all DBs
mc cp minio/pg-backups/cluster-<timestamp>.sql.gz .
gunzip cluster-<timestamp>.sql.gz
psql -h $PGHOST -p $PGPORT -U postgres -f cluster-<timestamp>.sql

Verification

kubectl -n backups get cronjob pg-backup
kubectl -n backups logs job/pg-backup-<timestamp>
# List objects:
kubectl -n backups run --rm -it mc --image=alpine:3.20 -- sh -lc '
  apk add --no-cache minio-client &&
  mc alias set minio "$MC_HOST_minio" &&
  mc ls minio/pg-backups'

Troubleshooting

- `/bin/sh: MINIO_ACCESS_KEY: parameter not set` → use MC_HOST_minio, not old env var names.
- `AccessDenied` → wrong ACCESS/SECRET or bucket policy; test with `mc alias set`.
- Pod OOM/ephemeral storage errors → increase resource limits; run off-peak.
- Network/TLS issues → verify service DNS names and CA trust to MinIO.

Security

- Use a write-only MinIO key scoped to the backup bucket.
- Enable SSE (server-side encryption) or KMS.
- Keep DB creds in Secrets; avoid storing in images/logs.
- Consider PITR with WAL archiving for production RPO < 24h.

Meme idea

Backups are like umbrellas—you miss them most when the sky opens up.

Taylor Swift

“I remember it all too well.” — All Too Well

Comments

Popular posts from this blog

Learning to Automate My Side Projects with SWE-agent + GitLab

Ship-Ready Web Essentials: Search, Sitemap, Metadata & Icons (SvelteKit)

Kubernetes Secrets Management — SOPS + age (GitOps‑friendly)