stack.basicstack.de/apps/stalwart/stalwart-postgres-backup.yaml
CTO Agent 9fe442ce0d feat(stalwart): deploy stalwart-postgres StatefulSet + backup (DEV-469)
Phase 2 of the Stalwart RocksDB -> PostgreSQL migration (DEV-467).
Purely additive - does not touch the running stalwart-0 pod, its
ConfigMap, PVC, or bootstrap-config.

- stalwart-postgres.yaml: single-replica StatefulSet with a 10Gi
  hcloud-volumes-encrypted PVC (subPath pgdata), ClusterIP Service
  on 5432, nodeAffinity csi.hetzner.cloud/location=fsn1 so the DB
  co-locates with stalwart-0 (which is fsn1-pinned by its PVC).
  Resources match forgejo-postgres (250m/512Mi req, 500m/1Gi lim).
- stalwart-postgres-credentials-sealed.yaml: sealed secret with
  POSTGRES_USER=stalwart, POSTGRES_DB=stalwart, POSTGRES_PASSWORD,
  plus a copy of the password under stalwart-db-password for
  Stalwart's [store.postgres] config in the Phase 4 cutover.
- stalwart-postgres-backup.yaml: daily CronJob at 02:30 UTC that
  streams pg_dump | gzip into the existing stalwart-s3-backup restic
  repo tagged stalwart-postgres, with independent retention keys
  (14d/8w/6m) so it doesn't collide with the RocksDB snapshots.
  activeDeadlineSeconds=1800 mirrors the DEV-464 sibling fix.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-15 12:05:25 +00:00

120 lines
4 KiB
YAML

---
apiVersion: batch/v1
kind: CronJob
metadata:
name: stalwart-postgres-backup
namespace: stalwart
labels:
app: stalwart-postgres-backup
spec:
schedule: "30 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 1800
template:
metadata:
labels:
app: stalwart-postgres-backup
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: postgres:16-alpine
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- |
set -euo pipefail
BACKUP_START=$(date +%s)
echo "=== Starting Stalwart Postgres backup at $(date -u) ==="
# pg_dump already in image; restic is small and only needed here.
apk add --no-cache restic
export RESTIC_REPOSITORY="s3:${S3_ENDPOINT}/${S3_BUCKET}/stalwart"
echo "Repository: ${RESTIC_REPOSITORY}"
# Repo is shared with the RocksDB backup, so it already exists;
# init only if the RocksDB side has not created it yet.
restic snapshots > /dev/null 2>&1 || restic init
echo "Streaming pg_dump | gzip -> restic (tag stalwart-postgres)..."
PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump \
-h stalwart-postgres.stalwart.svc.cluster.local \
-U "${POSTGRES_USER}" \
-d "${POSTGRES_DB}" \
--no-owner --no-acl \
| gzip -c \
| restic backup \
--stdin \
--stdin-filename stalwart-postgres.sql.gz \
--tag stalwart-postgres \
--tag daily \
--host stalwart-postgres-k8s
echo "Forget/prune (tag=stalwart-postgres, independent retention)..."
restic forget \
--tag stalwart-postgres \
--keep-daily 14 \
--keep-weekly 8 \
--keep-monthly 6 \
--prune
echo "Latest stalwart-postgres snapshots:"
restic snapshots --tag stalwart-postgres --compact || true
BACKUP_END=$(date +%s)
echo "=== Postgres backup complete in $((BACKUP_END - BACKUP_START))s at $(date -u) ==="
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: stalwart-postgres-credentials
key: POSTGRES_USER
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: stalwart-postgres-credentials
key: POSTGRES_DB
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: stalwart-postgres-credentials
key: POSTGRES_PASSWORD
- name: RESTIC_PASSWORD
valueFrom:
secretKeyRef:
name: stalwart-s3-backup
key: restic-password
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: stalwart-s3-backup
key: access-key
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: stalwart-s3-backup
key: secret-key
- name: S3_ENDPOINT
valueFrom:
secretKeyRef:
name: stalwart-s3-backup
key: endpoint
- name: S3_BUCKET
valueFrom:
secretKeyRef:
name: stalwart-s3-backup
key: bucket
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi