#!/usr/bin/env bash # Nightly backup: MySQL dump + uploads/exports -> S3 (versioned bucket). # # Install on the server (as the user that runs docker): # crontab -e # 30 2 * * * /opt/ar-aging/ar-aging-app/deploy/backup.sh >> /var/log/ar-backup.log 2>&1 # # Requires: aws cli v2 on the host, an instance IAM role with s3:PutObject/ListBucket on # the bucket (no access keys on disk), and .env.production next to docker-compose.prod.yml. set -euo pipefail APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$APP_DIR" # shellcheck disable=SC1091 set -a; source .env.production; set +a BUCKET="${AR_BACKUP_S3_BUCKET:?AR_BACKUP_S3_BUCKET not set in .env.production}" STAMP="$(date +%Y-%m-%d_%H%M)" COMPOSE="docker compose --env-file .env.production -f docker-compose.prod.yml" echo "[$STAMP] backup starting" # 1) MySQL dump (single transaction: consistent snapshot without locking the app out). $COMPOSE exec -T mysql sh -c \ 'exec mysqldump --single-transaction --quick --routines \ -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' \ | gzip > "/tmp/ar_aging_${STAMP}.sql.gz" aws s3 cp "/tmp/ar_aging_${STAMP}.sql.gz" "$BUCKET/mysql/ar_aging_${STAMP}.sql.gz" rm -f "/tmp/ar_aging_${STAMP}.sql.gz" # 2) Uploaded source files + generated exports (the audit trail). # The ar_data volume is mounted by the backend container; sync straight from it. DATA_MOUNT="$(docker volume inspect -f '{{ .Mountpoint }}' \ "$(basename "$APP_DIR" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9')_ar_data" 2>/dev/null \ || docker volume inspect -f '{{ .Mountpoint }}' ar-aging-app_ar_data)" aws s3 sync "$DATA_MOUNT/uploads" "$BUCKET/data/uploads" --only-show-errors aws s3 sync "$DATA_MOUNT/exports" "$BUCKET/data/exports" --only-show-errors echo "[$STAMP] backup finished" # Restore drill (run quarterly — a backup you never restored is a hope, not a backup): # aws s3 cp "$BUCKET/mysql/.sql.gz" - | gunzip | \ # docker compose --env-file .env.production -f docker-compose.prod.yml exec -T mysql \ # sh -c 'exec mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"'