Step-by-Step Guide to Creating a Robust Database Backup Script in PHP with Automated Cron

By Admin • Updated September 24, 2026 • 63 Reads
Robust MySQL database backup script in PHP with gzip compression and cron scheduling
Automated MySQL Database Backup Script in PHP

Hardware crashes, ransomware infections, corrupted migrations, and accidental DROP TABLE queries executed by sleep-deprived engineers happen without warning. Despite their critical importance, many development teams rely entirely on manual cPanel exports or simplistic graphical plugins that time out silently on databases larger than a few hundred megabytes.

A truly reliable disaster recovery strategy requires an automated, self-healing backup pipeline. In this complete engineering guide, we will design and deploy an enterprise-grade MySQL database backup script written in modern PHP 8.x. We will utilize native mysqldump with non-blocking transactional isolation, compress dumps with streaming Gzip, encrypt archives at rest using AES-256-CBC, implement automated retention pruning, and configure cron triggers with instant Telegram and Discord failure webhooks.

1. Why mysqldump Beats Iterative PHP SQL Scripts Every Time

Beginner developers often attempt to build backup scripts using PHP loops: querying SHOW TABLES, executing SELECT * FROM table, and concatenating SQL strings. This approach suffers from fatal architectural flaws:

  • Table Locking & Downtime: Iterative PHP queries lack atomic transaction snapshots. If customers place orders while your script iterates, your backup captures mismatched foreign keys and corrupts transactional consistency.
  • Memory Exhaustion & Timeouts: Exporting millions of rows through PHP memory easily hits memory_limit and max_execution_time barriers.
  • mysqldump Performance: The native mysqldump utility is a highly optimized C binary. By pairing it with the --single-transaction and --quick flags, it streams row data directly from InnoDB's Multi-Version Concurrency Control (MVCC) engine without locking tables or interrupting active users.

2. The 3-2-1 Enterprise Backup Rule

Before writing code, understand the golden rule of systems administration:

3 Copies of Data

Keep the primary production database plus at least two complete, restorable backup copies.

2 Different Media Types

Store backups across distinct storage mediums (e.g., local server SSD and remote cloud object storage like AWS S3 or Cloudflare R2).

1 Offsite Location

Ensure at least one copy is isolated in a separate geographic region to survive datacenter fires or account compromises.

3. Complete Production-Ready PHP Backup Engine

Below is the battle-tested, standalone backup script designed for execution via server cron or secure CLI runners:

<?php
declare(strict_types=1);

// Prevent unauthorized execution via web browser unless authorized token provided
if (php_sapi_name() !== 'cli') {
    $authSecret = 'CHANGE_THIS_TO_A_64_CHAR_HEX_KEY';
    if (!isset($_GET['token']) || !hash_equals($authSecret, (string)$_GET['token'])) {
        http_response_code(403);
        die("Access Denied: Unauthorized execution.");
    }
}

// Configuration
$dbHost     = '127.0.0.1';
$dbName     = 'production_database';
$dbUser     = 'backup_service_user';
$dbPass     = 'ComplexSecurePassword#2026';
$backupDir  = '/home/swabistore/secure_backups';
$retention  = 14; // Keep backups for 14 days
$alertWebhook = 'https://api.telegram.org/botYOUR_BOT_TOKEN/sendMessage?chat_id=YOUR_CHAT_ID';

// Ensure backup storage directory exists and has hardened permissions
if (!is_dir($backupDir)) {
    if (!mkdir($backupDir, 0700, true)) {
        sendWebhookAlert($alertWebhook, "BACKUP CRITICAL: Failed to create storage directory.");
        die("Fatal: Cannot create backup directory.\n");
    }
}

$timestamp  = date('Y-m-d_H-i-s');
$dumpFile   = sprintf('%s/%s_%s.sql.gz', $backupDir, $dbName, $timestamp);
$startTime  = microtime(true);

echo "[INFO] Starting database backup for: {$dbName}\n";

// Execute mysqldump with non-blocking single-transaction flags
$command = sprintf(
    'mysqldump --host=%s --user=%s --password=%s --single-transaction --quick --skip-lock-tables %s | gzip -9 > %s',
    escapeshellarg($dbHost),
    escapeshellarg($dbUser),
    escapeshellarg($dbPass),
    escapeshellarg($dbName),
    escapeshellarg($dumpFile)
);

$output = [];
$exitCode = 0;
exec($command, $output, $exitCode);

$duration = round(microtime(true) - $startTime, 2);

if ($exitCode !== 0 || !file_exists($dumpFile) || filesize($dumpFile) === 0) {
    $msg = "BACKUP FAILED for {$dbName}! Exit code: {$exitCode}. Duration: {$duration}s.";
    echo "[ERROR] {$msg}\n";
    sendWebhookAlert($alertWebhook, $msg);
    exit(1);
}

$fileSizeMb = round(filesize($dumpFile) / 1048576, 2);
$successMsg = "BACKUP SUCCESS: {$dbName} saved ({$fileSizeMb} MB) in {$duration}s.";
echo "[SUCCESS] {$successMsg}\n";

// Automated retention pruning: delete archives older than $retention days
echo "[INFO] Pruning archives older than {$retention} days...\n";
$now = time();
$prunedCount = 0;

$archives = glob($backupDir . '/*.sql.gz');
if ($archives !== false) {
    foreach ($archives as $archive) {
        if (is_file($archive)) {
            $fileAge = ($now - filemtime($archive)) / 86400;
            if ($fileAge > $retention) {
                unlink($archive);
                $prunedCount++;
                echo "[PRUNED] Deleted old archive: " . basename($archive) . "\n";
            }
        }
    }
}

echo "[INFO] Cleanup complete. Removed {$prunedCount} old archives.\n";

// Telemetry Notification Function
function sendWebhookAlert(string $url, string $text): void
{
    if (empty($url)) return;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url . '&text=' . urlencode($text));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_exec($ch);
    curl_close($ch);
}

4. Encrypting Database Backups at Rest with OpenSSL AES-256

If an attacker compromises your web server through an application-level vulnerability, plain SQL dumps stored on disk give them complete, unencrypted access to user emails, password hashes, and billing histories.

You can pipe the compressed stream directly through OpenSSL to produce a cryptographically armored file that cannot be decrypted without your private master key:

// Encrypt backup file using AES-256-CBC with PBKDF2 key derivation
$encFile = $dumpFile . '.enc';
$passphrase = 'YOUR_HIGH_ENTROPY_ENCRYPTION_PASSPHRASE';

$encCmd = sprintf(
    'openssl enc -aes-256-cbc -salt -pbkdf2 -in %s -out %s -k %s && rm -f %s',
    escapeshellarg($dumpFile),
    escapeshellarg($encFile),
    escapeshellarg($passphrase),
    escapeshellarg($dumpFile)
);
exec($encCmd);

5. Automated Offsite Cloud Replication to Cloudflare R2 / AWS S3

Keeping backup archives on the same physical SSD server as your web application violates the 3-2-1 rule. If the hosting provider's node suffers catastrophic hardware failure, your backups perish along with the database.

You can push the encrypted backup archive directly to an S3-compatible bucket (such as Cloudflare R2 or AWS S3) immediately after dump creation using an automated cURL stream:

function uploadToS3CompatibleStorage(string $filePath, string $bucket, string $endpoint, string $key, string $secret): bool
{
    $fileName = basename($filePath);
    $uri = "https://{$bucket}.{$endpoint}/{$fileName}";
    $date = gmdate('Ymd\THis\Z');
    
    $fileData = file_get_contents($filePath);
    $payloadHash = hash('sha256', $fileData);

    $ch = curl_init($uri);
    curl_setopt_array($ch, [
        CURLOPT_PUT => true,
        CURLOPT_INFILE => fopen($filePath, 'rb'),
        CURLOPT_INFILESIZE => filesize($filePath),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 300,
        CURLOPT_HTTPHEADER => [
            "x-amz-date: {$date}",
            "x-amz-content-sha256: {$payloadHash}",
            "Content-Type: application/octet-stream"
        ]
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ($httpCode === 200 || $httpCode === 201);
}

6. Point-in-Time Recovery (PITR) with MySQL Binary Logging

Daily dumps take a snapshot at 02:00 AM. But what happens if an engineer accidentally truncates a core table at 04:30 PM? Relying exclusively on daily dumps means losing 14 hours of customer orders and transactions.

To prevent data loss between scheduled dumps, enable MySQL Binary Logging (binlog) in your my.cnf server configuration:

[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 7
max_binlog_size = 100M
binlog_format = ROW

With binary logging enabled, MySQL records every single INSERT, UPDATE, and DELETE transaction to disk. To restore your database up to the exact minute of an incident:

  1. Restore the midnight snapshot: mysql -u root -p database < backup_midnight.sql
  2. Replay the binary logs up to the exact minute before the error:
    mysqlbinlog --stop-datetime="2026-09-24 16:29:59" /var/log/mysql/mysql-bin.0000* | mysql -u root -p database

7. Configuring the Automated Cron Schedule in Linux / cPanel

To automate the backup every midnight at 02:00 AM (when server traffic is lowest), configure the server cron table:

# Open server cron table:
crontab -e

# Run database backup every night at 2:00 AM UTC
0 2 * * * /usr/bin/php /home/swabistore/scripts/db_backup.php >> /home/swabistore/secure_backups/backup.log 2>&1

Summary: The Disaster Recovery Verification Test

Remember: An untested backup is not a backup!

Schedule a quarterly drill to test full restoration into a staging environment:

# Test restoration in staging database:
gunzip < backup_production_database_2026-09-24.sql.gz | mysql -u staging_user -p staging_db