DEV Community

Edgaras
Edgaras

Posted on

Laravel Project Backup to Azure Blob with Bash Script

If you’re running a Laravel app and want an easy way to back it up to Azure Blob Storage without Docker images or third-party SaaS tools, here’s a self-contained Bash script that does exactly that.

It backs up:

  • Your Laravel project files (with exclusions like vendor/ and logs)
  • Your MySQL database (via mysqldump)
  • Packages everything into a .tar.gz
  • Uploads to Azure Blob Storage using az CLI
  • Cleans up local backups older than X days

Requirements

  • bash, tar, gzip, find
  • mysqldump (MySQL/MariaDB client)
  • Azure CLI

The Script

#!/usr/bin/env bash set -Eeuo pipefail # ---- Paths ---- LARAVEL_DIR="/path/to/laravel/project" # Path to your Laravel project root BACKUP_DIR="/path/to/local/backup/directory" # Local backup directory  # ---- Azure Storage (account + key) ---- AZURE_STORAGE_ACCOUNT="<ACCOUNT_NAME>" AZURE_STORAGE_KEY="<ACCOUNT-KEY>" AZURE_CONTAINER="<CONTAINER-NAME>" # ---- Retention (local only) ---- RETAIN_LOCAL_DAYS=7 # ---- Backup content exclusion ---- EXCLUDES=( "vendor" "node_modules" "storage/framework/cache" "storage/logs" ".git" ) ############################################# timestamp_utc() { date -u +"%Y%m%dT%H%M%SZ"; } require_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: Missing required command: $1" >&2; exit 1; } } read_env_var() { local key="$1" local val="" if [[ -f "$LARAVEL_DIR/.env" ]]; then val="$(grep -E "^${key}=" "$LARAVEL_DIR/.env" 2>/dev/null | tail -n1 | sed -E "s/^${key}=(.*)$/\1/" | tr -d '\r' || true)" fi if [[ -n "$val" && "${val:0:1}" == '"' && "${val: -1}" == '"' ]]; then val="${val:1:-1}" elif [[ -n "$val" && "${val:0:1}" == "'" && "${val: -1}" == "'" ]]; then val="${val:1:-1}" fi echo "$val" } dump_mysql() { local db_host db_port db_name db_user db_pass db_socket dump_path db_host="$(read_env_var "DB_HOST")" db_port="$(read_env_var "DB_PORT")" db_name="$(read_env_var "DB_DATABASE")" db_user="$(read_env_var "DB_USERNAME")" db_pass="$(read_env_var "DB_PASSWORD")" db_socket="$(read_env_var "DB_SOCKET")" dump_path="$1" require_cmd mysqldump db_host="${db_host:-127.0.0.1}" db_port="${db_port:-3306}" local args=( --user="$db_user" --single-transaction --quick --routines --events --triggers ) if [[ -n "$db_socket" ]]; then args+=( --socket="$db_socket" ) else args+=( --host="$db_host" --port="$db_port" ) fi umask 077 MYSQL_PWD="$db_pass" mysqldump "${args[@]}" "$db_name" > "$dump_path" } make_archive() { local src_dir="$1" local db_sql="$2" local out_tar_gz="$3" local stage stage="$(mktemp -d)" trap 'rm -rf "$stage"' RETURN mkdir -p "$stage/site" "$stage/db" pushd "$src_dir" >/dev/null local tar_excludes=() for e in "${EXCLUDES[@]}"; do tar_excludes+=( "--exclude=$e" ) done tar -cf - "${tar_excludes[@]}" . | tar -C "$stage/site" -xf - popd >/dev/null cp "$db_sql" "$stage/db/database.sql" tar -C "$stage" -czf "$out_tar_gz" site db } upload_with_azcli() { local file="$1" AZURE_STORAGE_ACCOUNT="$AZURE_STORAGE_ACCOUNT" AZURE_STORAGE_KEY="$AZURE_STORAGE_KEY" \ az storage blob upload \ --container-name "$AZURE_CONTAINER" \ --file "$file" \ --name "$(basename "$file")" \ --overwrite true >/dev/null } main() { [[ -d "$LARAVEL_DIR" ]] || { echo "ERROR: LARAVEL_DIR not found: $LARAVEL_DIR" >&2; exit 1; } mkdir -p "$BACKUP_DIR" local ts app_name base_name db_dump archive ts="$(timestamp_utc)" app_name="$(basename "$LARAVEL_DIR")" base_name="${app_name}-${ts}" db_dump="${BACKUP_DIR}/${base_name}.sql" archive="${BACKUP_DIR}/${base_name}.tar.gz" echo "[1/4] Dumping MySQL → $db_dump" dump_mysql "$db_dump" echo "[2/4] Creating archive → $archive" make_archive "$LARAVEL_DIR" "$db_dump" "$archive" echo "[3/4] Uploading to Azure via az CLI" upload_with_azcli "$archive" echo "Upload complete: $(basename "$archive")" echo "[4/4] Pruning local backups older than ${RETAIN_LOCAL_DAYS}d" find "$BACKUP_DIR" -type f -name "${app_name}-*.tar.gz" -mtime +"$RETAIN_LOCAL_DAYS" -print -delete || true find "$BACKUP_DIR" -type f -name "${app_name}-*.sql" -mtime +"$RETAIN_LOCAL_DAYS" -print -delete || true echo "Done." } main "$@" 
Enter fullscreen mode Exit fullscreen mode

How It Works

  1. Database Dump

    • Reads DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD from .env
    • Runs mysqldump
  2. File Packaging

    • Copies project files while excluding unnecessary directories
    • Adds database.sql
    • Compresses into tar.gz
  3. Azure Upload

    • Uses az storage blob upload with AZURE_STORAGE_ACCOUNT + AZURE_STORAGE_KEY
  4. Retention Cleanup

    • Deletes .sql and .tar.gz files older than RETAIN_LOCAL_DAYS

Usage

Save the script:

nano laravel-backup.sh chmod +x laravel-backup.sh 
Enter fullscreen mode Exit fullscreen mode

Configure variables:

  • LARAVEL_DIR
  • BACKUP_DIR
  • AZURE_STORAGE_ACCOUNT
  • AZURE_STORAGE_KEY
  • AZURE_CONTAINER

Run manually:

./laravel-backup.sh 
Enter fullscreen mode Exit fullscreen mode

Or schedule with cron:

crontab -e 0 3 * * * /path/to/laravel-backup.sh >> /var/log/laravel-backup.log 2>&1 
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
gbhorwood profile image
grant horwood

if anyone wants to push their tarballs to s3 instead, i cover uploading to s3 with curl in bash here:
dev.to/gbhorwood/uploading-to-s3-w...

if your tarballs are too big for either azure or s3, i go over how to split them easily here:
dev.to/gbhorwood/bash-splitting-ta...