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

Backup Laravel Package

zenstruck/backup

A simple, configurable backup package for Laravel that archives your application’s database and files, stores backups on any Laravel filesystem disk, and integrates with the scheduler for automated backups.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer and confirm two runtime dependencies early: a working tar/zip binary on the host (the library shells out for archiving) and a writable temp directory with room for the largest dump.

composer require zenstruck/backup

Where to look first, in order:

  1. README.md — the fluent builder examples are the fastest path to a working setup.
  2. src/ — read BackupManager, Profile, and the Source/Destination/Processor contracts. These four concepts are the whole architecture; everything else is an implementation of one of them.
  3. The bundled console command (Symfony users get it via the companion bundle). Run --help before writing your own cron wrapper — you often don't need one.

First use case — the one you should build before anything else: a nightly archive of a directory plus a database dump, written to local disk and pushed off-box.

The assembly is intentionally boring and explicit:

$manager = new BackupManager(/* scratch dir */ '/var/tmp/backups');

$manager->createProfile('nightly')
    ->addSource(new FileSource('/var/www/app/storage'))
    ->addSource(new DatabaseSource('mysql', $dsn, ['gzip' => true]))
    ->addDestination(new FlysystemDestination($localFs))     // fast, same-host
    ->addDestination(new FlysystemDestination($s3Fs));        // offsite

$manager->backup('nightly');   // returns the created archive name

Get this printing a real archive name before adding retention, scheduling, or notifications. Every later problem is easier to debug from a known-good single profile.

Class names shift between minor versions in a package this small. Treat the snippets here as shape, not gospel — confirm signatures against src/ for the version you pin.

Implementation Patterns

One profile per backup contract, not per source. Name profiles after what the consumer expects (nightly-db, weekly-full, pre-deploy), then vary sources inside them. This keeps cron entries, retention rules, and restore runbooks aligned with a single string.

Fan-out destinations, fail loudly. Register the local destination first and the remote second so a remote outage still leaves you a recoverable copy on the box. Wrap the call and let exceptions escape — silent backup failures are the classic incident:

try {
    $archive = $manager->backup('nightly');
    $logger->info('backup.ok', ['archive' => $archive]);
} catch (\Throwable $e) {
    $logger->error('backup.failed', ['exception' => $e]);
    exit(1);   // non-zero so cron/monitoring notices
}

Retention as a separate, idempotent step. The library creates; it generally does not prune. Keep deletion out of the backup path and run it after a successful upload:

// keep 7 dailies, 4 weeklies — compute from the archive name's timestamp prefix
foreach ($destination->listArchives('nightly') as $name) {
    if ($olderThanPolicy($name)) { $destination->remove($name); }
}

Deriving age from the filename (UTC Ymd-His prefix) rather than from stat() mtimes survives copies across filesystems, where mtimes are frequently lost or rewritten.

Guard against overlap with a lock. A slow database dump plus a 5-minute cron is how you get three concurrent mysqldump processes and a dead box. Use symfony/lock around the whole profile execution and skip the run if the lock is held.

Test against an in-memory Flysystem adapter. Because destinations go through Flysystem, League\Flysystem\InMemory\InMemoryFilesystemAdapter lets you assert in a unit test that the archive exists and is non-empty — no S3 credentials, no temp file cleanup, and fast enough for CI.

Compose for one-off jobs. For pre-deploy snapshots, build a throwaway profile in the deploy script rather than growing a permanent one. Profiles are cheap; ambiguity about what nightly contains is not.

Gotchas and Tips

Temp space is the real constraint. Dumps and archives land in the scratch directory before upload. A 4 GB database on a 3 GB /tmp fails at 90% with an opaque archive error. Point the scratch dir at a volume sized to roughly 2.5× your largest source and make sure failed runs clean up after themselves — orphaned partial archives are the usual cause of "disk full again."

Filenames are your only index. There's no registry or metadata database here. That means the archive name must encode profile, UTC timestamp, and ideally a short git SHA or environment tag. Once you have thousands of files, renaming is a migration, so get the format right on day one.

Never delete before the remote write returns. Even with fan-out, a destination that throws mid-upload can leave a truncated object. Verify size (or re-list) on the remote before pruning local copies.

Credential handling. DSNs and Flysystem credentials belong in environment variables, never in the profile definition. If you're on Symfony, define the filesystem services in config/packages/flysystem.yaml and inject them — it also makes it trivial to point tests at a local bucket.

Shell-outs need an explicit PATH. mysqldump, pg_dump, and tar are resolved from PATH. Cron's PATH is not your shell's PATH. Use absolute paths in the dump configuration, or your backup works interactively and fails at 02:00.

Timeouts and memory. Long dumps exceed default Symfony Process timeouts. Set an explicit per-process timeout that is comfortably larger than your slowest observed run, and raise memory_limit if you're streaming large files through PHP rather than letting the CLI binary write directly.

Debugging approach. When an archive is wrong, reproduce with a single file source and a single local destination. If that works, add the database source; then add the remote. The failure is almost always in the most recently added component and the exception message from Flysystem or Process is usually buried one level down — check getPrevious().

Extension points. The useful customization surface is small and stable: implement a custom Source for anything that produces a file or stream (a Redis SAVE, an rsync pull, an API export), a custom Destination when Flysystem doesn't fit (e.g. a tape or cold-storage gateway), and a Processor if you need to transform the archive before it's written (encrypt, checksum). Prefer decorators over forking the library — it is small enough that patches upstream are slow relative to wrapping.

Pin and read the changelog. With a small dependent base, regressions surface later than you'd like and there is little community troubleshooting to search. Lock the version, and re-read the diff on upgrade — especially around the Source/Destination interfaces, which are the most likely place for a breaking change.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib