#!/usr/bin/env bash set -euo pipefail # ─── Configuration ────────────────────────────────────────────── BACKUP_DIR="${BACKUP_DIR:-./backups}" TIMESTAMP="$(date +%Y%m%d_%H%M%S)" BACKUP_PATH="${BACKUP_DIR}/${TIMESTAMP}" RETENTION_DAILY=7 RETENTION_WEEKLY=4 # S3-compatible backup bucket (optional) S3_ENDPOINT="${S3_BACKUP_ENDPOINT:-}" S3_BUCKET="${S3_BACKUP_BUCKET:-}" S3_ACCESS_KEY="${S3_BACKUP_ACCESS_KEY:-}" S3_SECRET_KEY="${S3_BACKUP_SECRET_KEY:-}" # ─── Color output ────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*"; } cleanup() { local exit_code=$? if [ $exit_code -ne 0 ]; then error "Backup script failed with exit code ${exit_code}" fi exit $exit_code } trap cleanup EXIT # ─── Ensure backup directory ─────────────────────────────────── mkdir -p "${BACKUP_PATH}" # ─── 1. PostgreSQL backup ─────────────────────────────────────── info "Backing up PostgreSQL..." POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-investplay-postgres}" POSTGRES_USER="${POSTGRES_USER:-investplay}" POSTGRES_DB="${POSTGRES_DB:-investplay}" if docker ps --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then docker exec "${POSTGRES_CONTAINER}" pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \ --no-owner --no-acl --compress=9 \ > "${BACKUP_PATH}/postgres_${TIMESTAMP}.sql.gz" 2>/dev/null || { warn "pg_dump failed — database may be temporarily unavailable" } ok "PostgreSQL backup saved" else warn "PostgreSQL container '${POSTGRES_CONTAINER}' not running — skipping" fi # ─── 2. Redis backup ──────────────────────────────────────────── info "Backing up Redis..." REDIS_CONTAINER="${REDIS_CONTAINER:-investplay-redis}" if docker ps --format '{{.Names}}' | grep -q "^${REDIS_CONTAINER}$"; then docker exec "${REDIS_CONTAINER}" redis-cli SAVE >/dev/null 2>&1 || { warn "Redis SAVE command failed" } # Copy the dump file from the container docker cp "${REDIS_CONTAINER}:/data/dump.rdb" "${BACKUP_PATH}/redis_${TIMESTAMP}.rdb" 2>/dev/null || { warn "Failed to copy Redis dump" } ok "Redis backup saved" else warn "Redis container '${REDIS_CONTAINER}' not running — skipping" fi # ─── 3. MinIO backup ──────────────────────────────────────────── info "Backing up MinIO..." MINIO_CONTAINER="${MINIO_CONTAINER:-investplay-minio}" MINIO_ALIAS="${MINIO_ALIAS:-investplay}" MINIO_USER="${MINIO_ROOT_USER:-minioadmin}" MINIO_PASS="${MINIO_ROOT_PASSWORD:-}" if docker ps --format '{{.Names}}' | grep -q "^${MINIO_CONTAINER}$"; then # Configure mc alias inside the container docker exec "${MINIO_CONTAINER}" mc alias set "${MINIO_ALIAS}" \ http://localhost:9000 "${MINIO_USER}" "${MINIO_PASS}" >/dev/null 2>&1 # Mirror all buckets to local backup directory mkdir -p "${BACKUP_PATH}/minio" docker exec "${MINIO_CONTAINER}" mc mirror --overwrite \ "${MINIO_ALIAS}/" "/tmp/minio-backup" >/dev/null 2>&1 || { warn "MinIO mc mirror failed" } docker cp "${MINIO_CONTAINER}:/tmp/minio-backup/." "${BACKUP_PATH}/minio/" 2>/dev/null || true docker exec "${MINIO_CONTAINER}" rm -rf /tmp/minio-backup 2>/dev/null || true ok "MinIO backup saved" else warn "MinIO container '${MINIO_CONTAINER}' not running — skipping" fi # ─── 4. Upload to S3-compatible backup bucket (optional) ──────── if [ -n "${S3_ENDPOINT}" ] && [ -n "${S3_BUCKET}" ]; then info "Uploading backup to S3-compatible storage..." if command -v mc &>/dev/null; then mc alias set backup-target "${S3_ENDPOINT}" "${S3_ACCESS_KEY}" "${S3_SECRET_KEY}" >/dev/null 2>&1 mc mirror "${BACKUP_PATH}" "backup-target/${S3_BUCKET}/investplay/${TIMESTAMP}" >/dev/null 2>&1 && { ok "Backup uploaded to S3" } || { warn "S3 upload failed" } elif command -v aws &>/dev/null; then AWS_ACCESS_KEY_ID="${S3_ACCESS_KEY}" AWS_SECRET_ACCESS_KEY="${S3_SECRET_KEY}" \ aws s3 cp --endpoint-url="${S3_ENDPOINT}" --recursive \ "${BACKUP_PATH}" "s3://${S3_BUCKET}/investplay/${TIMESTAMP}/" >/dev/null 2>&1 && { ok "Backup uploaded to S3" } || { warn "S3 upload failed" } else warn "Neither 'mc' nor 'aws' CLI found — skipping S3 upload" fi fi # ─── 5. Cleanup old backups ───────────────────────────────────── info "Cleaning up old backups..." total_backups=$(ls -1d "${BACKUP_DIR}/"20*/ 2>/dev/null | wc -l) # Keep last N daily backups ls -1d "${BACKUP_DIR}/"20*/ 2>/dev/null | sort -r | tail -n +$((RETENTION_DAILY + 1)) | while read -r dir; do rm -rf "${dir}" info "Removed old backup: $(basename "${dir}")" done # Keep last N weekly backups (every Sunday) if [ "$(date +%u)" -eq 7 ]; then ls -1d "${BACKUP_DIR}/"20*/ 2>/dev/null | sort -r | tail -n +$((RETENTION_WEEKLY + 1)) | while read -r dir; do rm -rf "${dir}" info "Removed weekly backup: $(basename "${dir}")" done fi ok "Cleanup complete" # ─── Summary ──────────────────────────────────────────────────── backup_size=$(du -sh "${BACKUP_PATH}" | cut -f1) echo "" echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}" echo -e "${GREEN} Backup completed successfully${NC}" echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}" echo "" echo -e " ${CYAN}Location:${NC} ${BACKUP_PATH}" echo -e " ${CYAN}Size:${NC} ${backup_size}" echo -e " ${CYAN}Date:${NC} $(date)" echo ""