#!/usr/bin/env bash set -Eeuo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_dir="$(cd "${script_dir}/.." && pwd)" env_file="${FRAMEFLOW_ENV_FILE:-${repo_dir}/.env.production}" backup_parent="${1:-${repo_dir}/backups}" metrics_dir="${FRAMEFLOW_BACKUP_METRICS_DIR:-/var/lib/frameflow/metrics}" timestamp="$(date -u +%Y%m%dT%H%M%SZ)" backup_dir="${backup_parent%/}/frameflow-${timestamp}" started_at_seconds="$(date +%s)" metrics_tmp="" cleanup() { if [[ -n "${metrics_tmp}" && -f "${metrics_tmp}" ]]; then rm -f "${metrics_tmp}" fi } trap cleanup EXIT if [[ ! -f "${env_file}" ]]; then echo "Production environment file not found: ${env_file}" >&2 exit 1 fi compose=(docker compose --env-file "${env_file}" -f "${repo_dir}/docker-compose.prod.yml") mkdir -p "${backup_parent}" if ! mkdir "${backup_dir}"; then echo "Refusing to overwrite an existing backup directory: ${backup_dir}" >&2 exit 1 fi postgres_container="$("${compose[@]}" ps -q postgres)" redis_container="$("${compose[@]}" ps -q redis)" minio_container="$("${compose[@]}" ps -q minio)" for container in "${postgres_container}" "${redis_container}" "${minio_container}"; do if [[ -z "${container}" ]]; then echo "PostgreSQL, Redis, and MinIO must be running before backup." >&2 exit 1 fi done echo "Creating PostgreSQL logical backup..." "${compose[@]}" exec -T postgres sh -c 'exec pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > "${backup_dir}/postgres.dump" echo "Flushing and archiving Redis persistence..." "${compose[@]}" exec -T redis sh -c ' if [ -n "${REDIS_PASSWORD:-}" ]; then redis-cli --no-auth-warning -a "$REDIS_PASSWORD" SAVE >/dev/null else redis-cli SAVE >/dev/null fi ' docker run --rm --volumes-from "${redis_container}" -v "${backup_dir}:/backup" alpine:3.20 \ tar -czf /backup/redis-data.tar.gz -C /data . echo "Archiving MinIO object data..." docker run --rm --volumes-from "${minio_container}" -v "${backup_dir}:/backup" alpine:3.20 \ tar -czf /backup/minio-data.tar.gz -C /data . { echo "created_at=${timestamp}" echo "compose_project=$(docker inspect --format '{{ index .Config.Labels "com.docker.compose.project" }}' "${postgres_container}")" echo "postgres_container=${postgres_container}" echo "redis_container=${redis_container}" echo "minio_container=${minio_container}" } > "${backup_dir}/manifest.txt" if command -v sha256sum >/dev/null 2>&1; then (cd "${backup_dir}" && sha256sum postgres.dump redis-data.tar.gz minio-data.tar.gz > SHA256SUMS) else (cd "${backup_dir}" && shasum -a 256 postgres.dump redis-data.tar.gz minio-data.tar.gz > SHA256SUMS) fi "${repo_dir}/scripts/verify-backup.sh" "${backup_dir}" completed_at_seconds="$(date +%s)" backup_size_bytes=0 for backup_file in postgres.dump redis-data.tar.gz minio-data.tar.gz manifest.txt SHA256SUMS; do file_size="$(wc -c < "${backup_dir}/${backup_file}" | tr -d '[:space:]')" backup_size_bytes=$((backup_size_bytes + file_size)) done mkdir -p "${metrics_dir}" metrics_tmp="$(mktemp "${metrics_dir%/}/frameflow_backup.prom.tmp.XXXXXX")" cat > "${metrics_tmp}" <