#!/usr/bin/env bash # # replicate-db.sh — Nightly prod_db -> dev_db replication with backups # Runs via cron as user "deploy" at 04:00 Kyiv time # set -euo pipefail # --- Lock (prevent parallel runs) --- LOCK_FILE="/tmp/replicate-db.lock" exec 200>"$LOCK_FILE" flock -n 200 || { echo "[$(date)] Another instance is already running. Exiting."; exit 1; } # --- Configuration --- POSTGRES_CONTAINER="postgres" SUPERUSER="app" SUPERUSER_PASS="zYWT5JWu3iAbbW7mOyd1" SOURCE_DB="prod_db" TARGET_DB="dev_db" TARGET_USER="dev_user" DEV_BACKEND_CONTAINER="gb-dev-backend" DEV_COMPOSE_DIR="/srv/apps/gb-site-dev/deploy" DEV_COMPOSE_FILE="docker-compose.dev.yml" BACKUP_DIR="/srv/backups" BACKUP_RETENTION_DAYS=14 LOG_DIR="/srv/logs" TIMESTAMP=$(date '+%Y%m%d-%H%M%S') LOG_FILE="${LOG_DIR}/replicate-db-${TIMESTAMP}.log" LOG_RETENTION_DAYS=30 CONTAINER_DUMP="/tmp/prod_dump.dump" # --- Logging --- mkdir -p "$LOG_DIR" "$BACKUP_DIR" log() { local ts ts=$(date '+%Y-%m-%d %H:%M:%S') echo "[$ts] $1" | tee -a "$LOG_FILE" } log_error() { local ts ts=$(date '+%Y-%m-%d %H:%M:%S') echo "[$ts] ERROR: $1" | tee -a "$LOG_FILE" >&2 } # --- Cleanup on exit --- cleanup() { local exit_code=$? # Remove dump from container log "Cleaning up dump file inside container..." docker exec "$POSTGRES_CONTAINER" rm -f "$CONTAINER_DUMP" 2>/dev/null || true # Always restart dev backend if ! docker ps --format '{{.Names}}' | grep -q "^${DEV_BACKEND_CONTAINER}$"; then log "Restarting dev backend..." cd "$DEV_COMPOSE_DIR" && docker compose -f "$DEV_COMPOSE_FILE" up -d gb-dev-backend 2>>"$LOG_FILE" || true fi # Clean old logs find "$LOG_DIR" -name "replicate-db-*.log" -mtime +${LOG_RETENTION_DAYS} -delete 2>/dev/null || true if [ $exit_code -eq 0 ]; then log "=== Replication completed successfully ===" else log_error "=== Replication FAILED with exit code $exit_code ===" fi } trap cleanup EXIT # ===== MAIN ===== log "=== Starting prod_db -> dev_db replication ===" # 1. Check postgres container log "Step 1: Checking postgres container..." if ! docker ps --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then log_error "PostgreSQL container is not running!" exit 1 fi # 2. Backup prod_db to host filesystem log "Step 2: Backing up prod_db..." BACKUP_FILE="${BACKUP_DIR}/prod_db_${TIMESTAMP}.dump" docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \ pg_dump -U "$SUPERUSER" -d "$SOURCE_DB" \ --format=custom \ --file=/tmp/prod_backup.dump \ 2>>"$LOG_FILE" docker cp "${POSTGRES_CONTAINER}:/tmp/prod_backup.dump" "$BACKUP_FILE" 2>>"$LOG_FILE" docker exec "$POSTGRES_CONTAINER" rm -f /tmp/prod_backup.dump 2>/dev/null || true BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) log "Backup saved: $BACKUP_FILE ($BACKUP_SIZE)" # 3. Rotate old backups log "Step 3: Rotating backups older than ${BACKUP_RETENTION_DAYS} days..." DELETED_COUNT=$(find "$BACKUP_DIR" -name "prod_db_*.dump" -mtime +${BACKUP_RETENTION_DAYS} -delete -print | wc -l) log "Deleted $DELETED_COUNT old backup(s)." # 4. Stop dev backend log "Step 4: Stopping dev backend..." docker stop "$DEV_BACKEND_CONTAINER" 2>>"$LOG_FILE" || log "Warning: dev backend was already stopped." sleep 2 # 5. Dump prod_db for replication (inside container) log "Step 5: Dumping prod_db for replication..." docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \ pg_dump -U "$SUPERUSER" -d "$SOURCE_DB" \ --format=custom \ --no-owner \ --no-acl \ --file="$CONTAINER_DUMP" \ 2>>"$LOG_FILE" log "Dump completed." # 6. Terminate connections to dev_db log "Step 6: Terminating connections to dev_db..." docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \ psql -U "$SUPERUSER" -d postgres -c \ "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${TARGET_DB}' AND pid <> pg_backend_pid();" \ >>"$LOG_FILE" 2>&1 || true # 7. Drop and recreate dev_db log "Step 7: Recreating dev_db..." docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \ psql -U "$SUPERUSER" -d postgres -c "DROP DATABASE IF EXISTS ${TARGET_DB};" \ >>"$LOG_FILE" 2>&1 docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \ psql -U "$SUPERUSER" -d postgres -c "CREATE DATABASE ${TARGET_DB} OWNER ${TARGET_USER};" \ >>"$LOG_FILE" 2>&1 log "Database recreated." # 8. Restore dump into dev_db log "Step 8: Restoring into dev_db..." docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \ pg_restore -U "$SUPERUSER" -d "$TARGET_DB" \ --no-owner \ --no-acl \ --role="$TARGET_USER" \ "$CONTAINER_DUMP" \ 2>>"$LOG_FILE" log "Restore completed." # 9. Grant privileges to dev_user # Note: --role=dev_user in pg_restore already sets object ownership log "Step 9: Granting privileges to dev_user..." docker exec -e PGPASSWORD="$SUPERUSER_PASS" "$POSTGRES_CONTAINER" \ psql -U "$SUPERUSER" -d "$TARGET_DB" -c " ALTER SCHEMA public OWNER TO ${TARGET_USER}; GRANT ALL PRIVILEGES ON DATABASE ${TARGET_DB} TO ${TARGET_USER}; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${TARGET_USER}; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${TARGET_USER}; GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${TARGET_USER}; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${TARGET_USER}; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${TARGET_USER}; " >>"$LOG_FILE" 2>&1 log "Privileges granted." # 10. Start dev backend log "Step 10: Starting dev backend..." cd "$DEV_COMPOSE_DIR" && docker compose -f "$DEV_COMPOSE_FILE" up -d gb-dev-backend \ 2>>"$LOG_FILE" log "Dev backend started." # 11. Health check log "Step 11: Health check..." sleep 5 HEALTH=$(curl -sf http://127.0.0.1:5200/api/health/ping 2>/dev/null || echo "FAILED") if [ "$HEALTH" = "FAILED" ]; then log "Warning: Health check did not respond (backend may still be starting)." else log "Health check OK: $HEALTH" fi log "=== Replication finished ==="