To set up automatic daily database backups in Laravel 13, install spatie/laravel-backup, publish its config, point the backup destination at a disk (ideally an offsite one such as S3), and schedule backup:clean and backup:run --only-db in routes/console.php. The Laravel scheduler, triggered by one cron entry, then creates a zipped database dump every day.
Backups are one of those things nobody thinks about until the day they are needed. The setup below takes about fifteen minutes. The part people skip, and should not, is the restore test at the end.
Prerequisites
- Laravel 13.x. Note that
spatie/laravel-backupv10 requires PHP 8.4 or newer with thezipextension, which is stricter than Laravel 13's own PHP 8.3 minimum. - The dump tool for your database on the server:
mysqldumpfor MySQL/MariaDB orpg_dumpfor PostgreSQL. - A Linux server for the scheduled job. The package documents that it does not support Windows servers.
- Enough free disk space for at least one full backup.
Step 1: Install the package
composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider" --tag=backup-configThis creates config/backup.php. The service provider registers itself.
Step 2: Create a dedicated backup disk
A backup stored on the same server as the database does not protect you from losing that server. Add a disk to config/filesystems.php. Here I use S3 with a separate bucket (install league/flysystem-aws-s3-v3 first):
'disks' => [
// ...
'backups' => [
'driver' => 's3',
'key' => env('BACKUP_AWS_ACCESS_KEY_ID'),
'secret' => env('BACKUP_AWS_SECRET_ACCESS_KEY'),
'region' => env('BACKUP_AWS_DEFAULT_REGION'),
'bucket' => env('BACKUP_AWS_BUCKET'),
'throw' => true,
],
],Give these credentials write access to the backup bucket only, so a leaked key cannot touch your other data.
Step 3: Configure what gets backed up
Open config/backup.php and adjust the key options:
'backup' => [
'name' => env('APP_NAME', 'laravel-backup'),
'source' => [
'files' => [
'include' => [base_path()],
// exclude vendor, node_modules, etc. (already in the default list)
],
'databases' => [env('DB_CONNECTION', 'mysql')],
],
'destination' => [
'disks' => ['backups'],
],
],If the dump binary is not on the default path, set dump.dump_binary_path on the connection in config/database.php. Only put the directory path there, not the binary name. Large databases may also need a longer dump.timeout than the default 60 seconds.
Step 4: Schedule the backup
In Laravel 13, scheduled tasks live in routes/console.php:
<?php
use Illuminate\Support\Facades\Schedule;
Schedule::command('backup:clean')->daily()->at('01:00');
Schedule::command('backup:run --only-db')
->daily()
->at('01:30')
->onOneServer()
->withoutOverlapping();
Schedule::command('backup:monitor')->daily()->at('09:00');--only-db skips application files, which usually live in Git anyway. Remove the flag to include files listed in source.files. onOneServer() stops duplicate backups when several servers run the scheduler; it requires a shared cache driver. The package recommends avoiding 02:00 to 03:00 if your server timezone uses daylight saving time.
Finally, make sure the scheduler runs every minute through cron on the server:
* * * * * cd /var/www/your-app && php artisan schedule:run >> /dev/null 2>&1How do I get notified if a backup fails?
The package sends notifications for failed backups, failed cleanups and unhealthy backups. Set the recipient in config/backup.php:
'notifications' => [
// ...
'mail' => [
'to' => 'ops@example.com',
],
],backup:monitor checks the monitor_backups rules, by default that the newest backup is not older than one day and the total size stays under a limit. It alerts you when the scheduled job silently stopped running, which a failure email alone cannot catch.
The default monitor entry watches the local disk. Change its disks value to ['backups'] so the health checks look at the disk you actually write to.
How do old backups get removed?
backup:clean applies the retention strategy in the cleanup section: keep all backups for a few days, then daily, weekly, monthly and yearly backups for set periods, and delete the oldest backups when the storage limit is reached. Tune keep_daily_backups_for_days and the related keys to your recovery needs.
Common mistakes
- Backing up to the local disk only. A disk failure or a compromised server takes the backups with it.
- Cron not configured. Without the
schedule:runentry nothing runs. Check withphp artisan schedule:list. mysqldump: command not found. Install the client tools or setdump_binary_path. In Docker images, the client is often missing.- Wrong PHP version. v10 needs PHP 8.4. Composer will refuse to install it on PHP 8.3.
- Never testing a restore. An untested backup is a hope, not a backup.
How to verify it works
- Run a backup by hand:
php artisan backup:run --only-db. The output reports the dump, the zip and the copy to each disk. - List backups and their health:
php artisan backup:list. - Confirm the schedule:
php artisan schedule:listshould show both commands with their next run time. - Do a restore drill: download the zip, unzip it, and import the SQL file into a fresh local database, for example with
mysql new_db < path/to/dump.sqlusing the.sqlfile from thedb-dumpsfolder. Then boot the app against it.
FAQ
Can I encrypt the backup archive?
Yes. Set BACKUP_ARCHIVE_PASSWORD in .env; the config reads it as the archive password. Store that password outside the server, or the backup is useless when the server is gone.
Can I back up to two places at once?
Yes. List several disks in destination.disks, for example ['local', 'backups'].
Does this replace managed database snapshots?
Not necessarily. Provider snapshots are great for full recovery; an application-level dump in another account or provider is a useful independent second copy.



