spatie/nova-backup-tool
A Laravel Nova tool that lists, creates, downloads, and deletes backups, powered by spatie/laravel-backup. Requires Nova and the backup package installed and configured.
spatie/nova-backup-tool is a UI layer over spatie/laravel-backup. Nothing works until the underlying package does. So the real first step is:
composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
php artisan backup:run # must produce a zip somewhere
php artisan backup:list # must list it
If those two commands don't work from the CLI, the Nova tool has nothing to show and you'll waste an afternoon debugging the wrong layer.
composer require spatie/nova-backup-tool
Register it in app/Providers/NovaServiceProvider.php:
public function tools(): array
{
return [
// ...
new \Spatie\BackupTool\BackupTool(),
];
}
A "Backups" menu item appears in the Nova sidebar. That's the whole install — no migrations, no assets to publish (the tool ships compiled JS via Nova's tool discovery).
config/backup.php — this is where behaviour actually lives: the backup name, the destination disks, what gets included/excluded, the DB connection, and the cleanup strategy. The Nova tool is a thin wrapper around these values; when the UI "doesn't show my disk" or "shows nothing", it's almost always this file.config/queue.php / QUEUE_CONNECTION — backups are dispatched to a queue. The README explicitly says: any driver except sync.php artisan queue:work. Without it, clicking "Create backup" spins forever.The most valuable day-one workflow is not "full backup", it's a fast, verified database-only snapshot:
backup.destination.disks).--only-db semantics, added in 3.1.0).db-dump.sql inside and that its size is plausible.That last step is the one people skip — and it's the only thing that distinguishes "I have backups" from "I have files on S3".
Do not replace your scheduler with the UI. The tool has no scheduler and no retention logic of its own. The pattern that works:
// routes/console.php (Laravel 11+) or app/Console/Kernel.php
Schedule::command('backup:run')->dailyAt('02:00');
Schedule::command('backup:clean')->dailyAt('03:00');
Schedule::command('backup:monitor')->dailyAt('04:00');
Then the Nova tool becomes the human interface: pre-migration snapshots, "grab me a copy of prod from last night", "delete that 40 GB artifact from March". This division of responsibility is exactly what the package is designed for.
The disk dropdown (added in 5.0.1) exists because real setups use more than one destination. A common production arrangement:
| Disk | Purpose | Notes |
|---|---|---|
backups-local |
Fast staging / disaster-of-the-day | Same host; useless if the host dies |
backups-s3 |
Durable offsite | Primary; what you actually restore from |
backups-glacier (or a second bucket) |
Long retention | Written by a separate scheduled job, not the UI |
Adding a disk to config/filesystems.php is not enough — it must also be listed in config/backup.php:
'destination' => [
'disks' => ['backups-local', 'backups-s3'],
],
Only those appear in the tool's selector.
Backups are heavy, long-running jobs and they shouldn't sit behind a thousand queued emails. Register the tool with a dedicated queue (supported since 3.9.0) and run a second worker:
// NovaServiceProvider
public function tools(): array
{
return [
new \Spatie\BackupTool\BackupTool(/* queue name */),
];
}
php artisan queue:work --queue=backups --timeout=1800 --tries=1
Check the BackupTool constructor signature in your installed version before wiring this up — the queue name has moved around between majors.
A download button on a production DB dump is a data-exfiltration feature for anyone with Nova access. Restrict the tool:
public function tools(): array
{
$tool = new \Spatie\BackupTool\BackupTool();
return [
$tool->canSee(fn ($request) => $request->user()?->hasRole('super-admin') ?? false),
];
}
If canSee() isn't available on the tool class in your Nova version, enforce it one layer up — a Gate check in Nova::auth() or a middleware around nova-vendor/backup-tool/* routes. Either way: never ship this visible to all Nova users.
// config/nova-backup-tool.php
'polling' => true,
'polling_interval' => 1, // seconds
Polling exists to make queued backups feel instant. Sensible per-environment settings:
1 second is fine.5, or set 'polling' => false entirely if you have several admins leaving the page open. Each poll re-lists the destinations — on S3 that's an API call every second, per open browser tab.There is no restore button. The tool creates, lists, downloads, deletes — that's the entire surface. Write the runbook and keep it in the repo:
# 1. Download the zip via the Backups UI
# 2. Unzip
unzip 2026-10-19-02-00-00.zip -d restore/
# 3. Database
mysql -h $DB_HOST -u $DB_USER -p $DB_NAME < restore/db-dump.sql
# 4. Files
rsync -av restore/ /var/www/app/storage/app/
Add a link to it in the tool's translated menu label or your internal wiki. The gap between "I can click Create backup" and "I can actually restore" is where incidents go wrong.
The tool ships translations (Spanish, German, Dutch, French, Persian, Arabic, Portuguese, Lithuanian…) and supports overriding them. To rename "Backups" to match your org's vocabulary ("Snapshots", "Sauvegardes"), publish and edit the tool's lang files, or override the JSON keys for en. Since 3.3.x the menu title follows the app locale.
The Nova tool is not an alerting system. It shows what exists; it does not tell you a backup failed. Configure notifications in config/backup.php (Slack/mail) and backup:monitor against a healthcheck endpoint. The tool's status overview shows the newest backup per disk — useful context, worthless as a pager.
sync queue trapThe README's warning is the #1 footgun. With QUEUE_CONNECTION=sync, the backup runs inside the HTTP request: the Nova page hangs, PHP hits max_execution_time, and you get a half-written zip or a 504. Symptom: clicking "Create backup" spins, then nothing appears.
grep QUEUE_CONNECTION .env # must not be sync
php artisan queue:work # must actually be running
Also confirm supervisor/systemd is managing the worker — a worker you started by hand dies with your SSH session.
The default queue:work --timeout=60 will kill a database dump of any non-trivial size. The job dies mid-write, the UI polls forever, and failed_jobs holds the evidence. Fix both sides:
--timeout=1800 (and retry_after in config/queue.php must be larger than the timeout, or the job gets executed twice).$timeout you can override in a subclass if you need it.config('backup.backup.name') (defaults to config('app.name')) prefixes every archive filename. The tool matches files by that pattern. Rename the app or change backup.name in backup.php, and yesterday's backups vanish from the UI — even though they're still sitting on the disk, perfectly restorable. If you must rename, move the existing files to the new prefix (aws s3 mv / rename) in the same change.
Delete removes the file from the destination disk — no trash, no undo, no versioning unless your bucket has it. Enable S3 versioning or a lifecycle rule you can recover from before you let multiple people drive this UI. There's also no "are you sure this is the last backup?" guard: it's entirely possible to delete your only copy. Changelog 4.1.6 fixed a flashing delete modal, not the underlying finality.
Clicking download proxies the file through your app server. For multi-gigabyte archives that means:
memory_limit and max_execution_time pressure,fastcgi_read_timeout / proxy_read_timeout truncation,For anything large, skip the button: aws s3 cp s3://bucket/2026-10-19.zip . or rsync from the on-prem box. Keep the UI for DB-only snapshots (typically hundreds of MB at most).
If your backups land on the local disk of an ephemeral container, every deploy wipes your backup directory — and the Nova list with it. On any containerized setup, the tool should point only at object storage. Check backup.destination.disks for stray local entries in production.
Changed config/backup.php and the UI still shows the old disk list? php artisan config:cache wasn't re-run (or was run before your edit). Same class of bug applies to nova-backup-tool's polling config: values are baked into the cached config.
Workers hold old code in memory. After deploying a new backup.php, a new tool registration, or a package upgrade, run php artisan queue:restart — otherwise the backup job executes against the previous release's configuration and disks.
php artisan route:list | grep backup-tool # routes registered?
php artisan queue:failed # job in failed_jobs?
tail -f storage/logs/laravel.log # dispatch error / missing class
php artisan backup:run --only-db # does the CLI side even work?
That sequence separates Nova-registration problems, queue problems, and laravel-backup problems — three very different fixes.
The tool's major versions track Nova/Laravel support, and the release notes show repeated compat fixes:
| Your stack | Use |
|---|---|
| Nova 5 / Laravel 12–13 | 6.0.4+ (6.0.3 fixed Laravel 13, 6.0.4 corrected it) |
| Nova 4 / Laravel 9–10 | 5.0.x |
| Nova 3 / Laravel 8 | 4.0.x |
| laravel-backup 6 | 2.0+ (1.1.1 "does not work for v6" — explicitly reverted) |
Don't chase the latest tag on an old Nova; a mismatched pair usually surfaces as a blank tool page or a Vue asset error in the console rather than a clean Composer failure.
This package is a Vue page plus a handful of controllers calling backup:run / backup:list / delete. There is no service container binding or event to hook. If you need something the UI doesn't do:
config/backup.php and your bucket lifecycle rules.BackupTool and override canSee/canRun, or add a Gate around the download route.Forking is cheap because the surface is small, but expect to re-merge on every Nova major.
The tool makes producing backups a one-click affair, which encourages exactly the wrong confidence. Put a quarterly reminder in the runbook: download the latest archive, restore it into a throwaway database, and boot the app against it. The Nova tool will happily keep showing you green rows for backups that are structurally broken.
How can I help you explore Laravel packages today?