nyoncode/laravel-backup-manager
Laravel Backup Manager is a Laravel package for organizing application backups in one place. It focuses on simplifying backup configuration, running and monitoring backup tasks, and integrating scheduled database and file backups into familiar Laravel workflows.
config/backup.php and find the profiles array — this is where nearly everything you will touch lives.name, schedule, destination (disk + path), and retention block. Note that path defaults to backups for every profile, so sharing a disk between profiles is the expected arrangement, not an edge case.php artisan backup:run --profile=default, then php artisan backup:list --profile=default. Confirm the listing shows only that profile's backups — as of v1.3.0 it is scoped by the profile recorded in each backup's manifest.default profile on the s3 disk with keep_latest = 7, plus a fortnightly full profile on the same disk with keep_latest = 5. Before v1.3.0 this pairing silently destroyed the full backups; you can now rely on it.Where to look first when something is off: backup:list --profile=X (what the profile thinks it owns), then the manifest inside the archive, then CleanupReport.
One profile per cadence, one destination per profile. The natural setup is default (nightly, short retention) and full (weekly/fortnightly, long retention) on the same disk. Retention is now counted per profile from the manifest tag, so the two policies no longer fight. If you want a shared blob pool (deduplicated snapshots), keep it — reference counting is per profile, and cleanups only delete what their own profile's keep-set releases.
Queue both halves of the lifecycle. BACKUP_QUEUE=true now dispatches CleanBackupsJob for backup:clean exactly as it does for backup:run. Schedule both without --sync so neither the backup nor the reference-counting cleanup runs inside the scheduler's process:
$schedule->command('backup:run --profile=default')->dailyAt('01:00');
$schedule->command('backup:clean --profile=default')->dailyAt('02:00');
Use --sync only for small destinations where you want the exit code inline (CI smoke tests, deploy hooks). --dry-run never queues — its product is the printed preview, so it always runs inline; that makes it safe and cheap to run from a tinker session or a cron on the web box.
Tune restore timeouts separately. Restores download, decrypt, unpack, and take a pre-restore snapshot, so they routinely outlast the backup job. Set queue.restore_timeout (BACKUP_QUEUE_RESTORE_TIMEOUT) to something generous — often 2–4× queue.timeout. Leaving it null inherits queue.timeout, which is the usual cause of a restore job being killed midway.
Call CleanupManager instead of reimplementing retention. The CLI, the queue, the dashboard, and the MCP tool all route through it now. If you have custom commands, health checks, or a "prune before deploy" step, resolve CleanupManager and apply the profile's policy there rather than hand-rolling a list-and-delete. You get the same two-engine sequence and a CleanupReport per destination that you can log or assert on.
Use StoredBackup::$profile / belongsTo() in your own tooling. Anything that walks a destination — a custom dashboard widget, an audit command, a Slack notifier — should filter with belongsTo($profile) rather than by path alone. This is the same predicate retention and listings use, so your reporting cannot drift from what actually gets deleted.
If you ship a custom SnapshotDriver, update the signature. applyRetention() now takes an optional trailing ?string $profile. Existing call sites keep working; your implementation must accept the parameter.
CI matrix with Pest. pestphp/pest is ^4.0|^5.0. Pest 5 requires PHP 8.4 + Symfony 8, so a Laravel 12 job stays on Pest 4 and only a Laravel 13 job picks up Pest 5. Pin per matrix leg rather than letting the resolver choose.
backup:clean --dry-run per profile first and look at the overlap; if it's not what you want, prune the legacy set once, manually, before letting scheduled cleanups run.null still means "every backup in the path". RetentionManager::apply() and applyRetention() treat a null/omitted $profile as the old global behaviour. That is what keeps existing calls working — and it is exactly the footgun the release fixed. Never let $profile be null by accident in new code; assert it or default it explicitly to 'default'.backup:list, backup:verify, and backup:restore no longer show a neighbouring profile's backups on a shared destination. If a restore can't find a file you can see on the disk, you're on the wrong profile — pass --profile.queue.queue now falls back to backups. Previously it dispatched onto a queue literally named '', which no worker consumes: jobs vanished with no failure record. If you were relying on the default connection's queue name, verify your worker listens on backups (or set queue.queue explicitly) after upgrading.failed_jobs. Watch that table and your queue dashboard after enabling BACKUP_QUEUE — a failing cleanup is silent in the backup log.--dry-run and --sync are orthogonal. --dry-run forces inline regardless of BACKUP_QUEUE; --sync forces inline for real runs. Mixing BACKUP_QUEUE=true with a scheduled backup:clean and no --sync is the intended production shape.CleanupReport for the destination → StoredBackup::belongsTo() in tinker against the actual manifest → confirm the profile name in config matches the one passed on the CLI (they are matched exactly, not normalised).backup:verify on a schedule.CleanupManager is the seam for anything retention-adjacent — custom reporting, alerting on CleanupReport, pre-prune hooks. Prefer it over subclassing RetentionManager, whose optional-profile parameter is now part of the contract.How can I help you explore Laravel packages today?