Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Backup Laravel Package

spatie/laravel-backup

Create a zip backup of your Laravel app's files and database, store it on any configured filesystem or multiple at once, monitor backup health, get notified when problems arise, and automatically clean up old backups. Run php artisan backup:run.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal install

composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"

This publishes config/backup.php. Before running anything, verify three things in that file:

  1. source.databases — the connections you actually want dumped.
  2. source.files.include — normally just base_path(), with exclude entries for vendor, node_modules, and your temp/backup directories.
  3. destination.disks — the Laravel filesystem disk(s) the zip lands on. Never point this at a disk whose local root is inside your own source tree.

Then make sure the dump binaries exist on the box (mysqldump, pg_dump, sqlite3 — whichever your driver needs) and that the web/queue user can execute them.

First use case

php artisan backup:run --only-db     # fastest smoke test
php artisan backup:list              # confirm the archive exists and its size

backup:list is your feedback loop: it reads the destination disks and reports backup name, disk, size, and newest-backup date. If backup:run succeeds but backup:list is empty, your problem is disk visibility, not backup creation.

Where to look first

  • config/backup.php for all behavior (sources, disks, retention, monitoring, notifications).
  • The docs site (spatie.be/docs/laravel-backup) for per-section detail — the README is deliberately thin.
  • The scheduled commands section, because almost nothing in this package is useful unless it runs on a schedule.

Note the version constraint: v10 targets PHP 8.4+/Laravel 12+ per the README, though 10.2.0 relaxed PHP to ^8.3. Pin deliberately and read UPGRADING.md before a major bump.

Implementation Patterns

Scheduling is the real integration. In routes/console.php (or Kernel::schedule):

use Illuminate\Support\Facades\Schedule;

Schedule::command('backup:run')->daily()->at('01:30');
Schedule::command('backup:clean')->daily()->at('02:30');
Schedule::command('backup:monitor')->daily()->at('03:00');

Order matters: run → clean → monitor. Monitor checks freshness, so running it before the nightly backup will alert you on a false positive every day. Add ->withoutOverlapping() and, on multi-server setups, ->onOneServer(). runInBackground() is fine for backup:run and backup:clean; keep backup:monitor in the foreground if you want its exit code to feed your own alerting.

Multi-destination for cheap insurance. Put a second disk in destination.disks and enable continue_on_failure so one unreachable disk doesn't abort the whole run:

'destination' => [
    'disks' => ['local', 's3'],
    'continue_on_failure' => true,
],

The archive is built once and written to each disk; a failure on s3 still leaves a valid local backup.

Encryption + verification for production. Set a passphrase and let the package encrypt while the zip is built (v10 behavior — no second write pass):

'encryption' => 'aes256',        // 'none' | 'default' | 'aes128' | 'aes192' | 'aes256'
'password' => env('BACKUP_PASSWORD'),
'verify_backup' => true,

verify_backup reopens the archive and, since 10.3.1, compares the file count against the backup manifest — it catches truncated archives, not just unopenable ones. Cheap enough to leave on for database backups.

Selective, ad-hoc runs. The command options are the day-to-day tools:

php artisan backup:run --only-db
php artisan backup:run --only-files
php artisan backup:run --db-name=mysql --db-name=secondary
php artisan backup:run --only-to-disk=s3
php artisan backup:run --filename=pre-migration.zip
php artisan backup:run --exclude=node_modules --exclude=storage/app/tmp
php artisan backup:run --destination-path=releases/2026-09

--filename before a risky deploy, followed by a manual verification that the archive opens, is the pattern that actually saves people.

Multiple configurations (--config). For a second destination set, a tenant-driven setup, or an alternate retention policy, publish a second config file (e.g. config/backup_secondary.php) and pass --config=backup_secondary. In v10 this flag reloads config at runtime; 10.3.2 added the same override to backup:clean so the cleanup strategy is resolved from the supplied config rather than the default one. Keep the two files structurally identical — missing keys are the usual failure mode with tenant-driven configs.

Notifications and events are decoupled (v10). Events always fire; notifications are gated separately via EventHandler::disable(). All events now carry primitives (string $diskName, string $backupName) rather than domain objects, which means queued listeners work correctly:

Event::listen(function (BackupWasSuccessful $event) {
    // $event->diskName, $event->backupName
});

For notifications, point notifications.notifications at your own subclasses of the package notification classes if you want custom copy or routing — that's the intended extension point, not rewriting the event pipeline. Channels cover mail, Slack, and (since v10) a generic webhook. Set log_channel to route backup activity logs to a dedicated channel, or false to silence them.

Monitoring configuration. monitor_backups is an array of named entries, each with its own disks and health checks — typically maximum age and maximum storage size. The name must match what your backups are actually written as; a mismatch means the monitor looks at nothing and reports unhealthy forever. v10 reports all health check failures (via getHealthCheckFailures()) instead of stopping at the first, which makes a single monitor run genuinely diagnostic.

Retention. The default strategy is day-count based:

'cleanup' => [
    'default_strategy' => [
        'keep_all_backups_for_days' => 7,
        'keep_daily_backups_for_days' => 16,
        'keep_weekly_backups_for_days' => 8,
        'keep_monthly_backups_for_months' => 4,
        'keep_yearly_backups_for_years' => 2,
        'delete_oldest_backups_when_using_more_megabytes_than' => 5000,
    ],
],

The size cap is the safety net — without it, a files-inclusive backup on a growing app will quietly fill a disk. Swap in a custom CleanupStrategy implementation if you need tiered offsite retention.

Restores are on you. This package creates backups; it does not restore them. Budget for a restore path (wnx/laravel-backup-restore exists for the common case, or document a manual unzip + mysql < + rsync procedure) and test it on a staging box. An unverified backup is a hypothesis, not a backup.

Gotchas and Tips

The temp directory needs headroom. The zip is assembled locally before being written to disks. A files-inclusive backup can easily need 1.5–2× the app size in storage/app/backup-temp (or whatever temporary_directory is set to). On small VPS boxes this is the #1 cause of failed backups. Also make sure that directory is excluded from your own sources, or you'll back up your backups.

Never let the destination disk live inside the source set. If local resolves to storage/app/private and that path is inside base_path() without an exclude, each backup contains the previous backups. Growth is exponential and it is not obvious until the disk is full.

mysqldump fails on permissions, not on the package. The classic error is the PROCESS privilege required for tablespace info. Add an extra option in source.databases.mysql.dump.add_extra_option ('--no-tablespaces') or grant the privilege. Also set dump_binary_path if mysqldump isn't on the web user's PATH — cron environments differ from your shell. exclude_tables was renamed to exclude_tables from excludeTables in 9.3.5; older configs silently do nothing.

Timeout and memory. Large file sets stress PHP's max_execution_time and the zip layer. Run via CLI (where timeouts are usually off) rather than through a web request, prefer --only-db for frequent backups and files on a slower cadence, and keep an eye on the archive count in backup:list after each config change rather than assuming success.

Encryption upgrade trap (10.3.0). EncryptBackupArchive is deprecated and no longer registered. If you had previously unregistered that listener to suppress encryption while keeping a password set, set encryption to 'none' instead — otherwise your backups are now encrypted and your restore procedure is broken. Conversely, if libzip lacks AES support, backup now fails loudly with BackupFailed rather than producing a plaintext archive. That's the correct behavior, but it will surface on upgrade.

v10 breaking changes worth pre-checking before the bump: events carry string $diskName/$backupName instead of BackupDestination objects; consoleOutput() is replaced by backupLogger(); ConsoleOutput is gone; BackupJob::disableNotifications() is replaced by EventHandler::disable(); CleanupStrategy now takes Config instead of Repository; encryption values are 'none'|'default'|'aes128'|'aes192'|'aes256' rather than null/false/'default'.

Monitor failures are usually config, not disaster. Before panicking: does monitor_backups[].name match the backup name produced by the run? Do the disks match destination.disks? Is the schedule actually firing (schedule:list, plus the cron entry)? Is the newest backup on that specific disk older than your max-age threshold because continue_on_failure masked that disk's failure? The health check reports a freshness/size/storage verdict, not the reason.

continue_on_failure hides disk outages. It's the right default for multi-destination, but it means a run can "succeed" while one destination silently has nothing new. Monitor each disk, not just the primary.

Storage-backed filesystems can be slow to list. backup:list, backup:monitor, and backup:clean all enumerate the disk. Against S3 with thousands of objects this takes real time; don't schedule backup:monitor every five minutes and don't call backup:list from a health endpoint.

Timezone drift in notifications was fixed in 9.4.0 — if you're pinned older, notification timestamps can look an hour or more off during DST.

Testing your config. For application tests, use Storage::fake() for the destination disk and assert against events (BackupWasSuccessful, BackupHasFailed) rather than shelling out. backup:run --only-db against a SQLite test connection is a fast end-to-end check in CI and validates your binary path. Enable verify_backup in CI runs specifically — it's the cheapest way to catch a config that produces structurally broken archives.

Exit codes. backup:run, backup:clean, and backup:monitor return non-zero on failure, which is what makes them usable in cron with || curl-style alerting. If you wrap them in Artisan::call() inside a job, you lose that — check the returned code explicitly.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin