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 Manager Laravel Package

frcho/backup-manager

Laravel package for backing up application databases and files to local or remote disks such as Amazon S3, Dropbox, and FTP. Configure multiple backup profiles, schedule automated backups, and restore data quickly with simple Artisan commands. Handles database dumps and directory archives.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm the package is even installable in your stack. Before writing any code, run composer require frcho/backup-manager in a throwaway branch and inspect composer.json inside vendor/frcho/backup-manager: check the autoload PSR-4 prefix (it may be BackupManager\ from upstream or Frcho\BackupManager\) and the require block for league/flysystem and PHP constraints. This fork has 0 stars, 0 dependents, and is effectively unmaintained — plan on pinning it, or vendoring the src/ directory into app/Support/Backup if the constraints block Laravel 9+ (Flysystem 3).

2. Read these files first, in order:

  • README.md — the config array shape and the two public entry points.
  • src/.../Manager.php — the only object you actually need.
  • src/.../Filesystems/FilesystemProvider.php — how storage adapters are registered.
  • src/.../Databases/DatabaseProvider.php — how dump/restore commands are built.
  • src/.../Config/Config.php — the array keys the library expects.

3. There is no Laravel service provider in this package (it targets Symfony2 and predates Laravel auto-discovery). You must wire it yourself. Add a config file and a singleton binding:

// config/backup.php
return [
    'database' => [
        'mysql' => [
            'type'     => 'mysql',
            'host'     => env('DB_HOST'),
            'port'     => env('DB_PORT', 3306),
            'user'     => env('DB_USERNAME'),
            'pass'     => env('DB_PASSWORD'),
            'database' => env('DB_DATABASE'),
        ],
    ],
    'storage' => [
        'local' => ['type' => 'Local', 'root' => storage_path('app/backups')],
        's3'    => [
            'type'   => 'AwsS3',
            'key'    => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
            'root'   => 'db-backups',
        ],
    ],
];
// app/Providers/BackupServiceProvider.php
public function register(): void
{
    $this->app->singleton(\BackupManager\Manager::class, function () {
        $config = new \BackupManager\Config\Config(config('backup'));

        $filesystems = new \BackupManager\Filesystems\FilesystemProvider($config);
        $filesystems->add(new \BackupManager\Filesystems\LocalFilesystem());
        $filesystems->add(new \BackupManager\Filesystems\AwsS3Filesystem());

        $databases = new \BackupManager\Databases\DatabaseProvider($config);
        $databases->add(new \BackupManager\Databases\MysqlDatabase());

        return new \BackupManager\Manager($filesystems, $databases, $config);
    });
}

4. First use case — a manual "back up to S3" Artisan command, run once locally to prove the toolchain:

public function handle(\BackupManager\Manager $manager)
{
    $manager->makeBackup()->run(
        'mysql',
        [['type' => 's3', 'path' => 'nightly/']],
        'gzip'
    );

    $this->info('Backup uploaded.');
}

Before running it, verify on the box that mysqldump --version and gzip --version resolve as the PHP user (see Gotchas). If php artisan backup:run produces a file in S3, you are done with setup — everything else is scheduling and hardening.

Implementation Patterns

Fan-out to multiple destinations in one call. The second argument is a list, so write local and offsite in a single dump:

$manager->makeBackup()->run('mysql', [
    ['type' => 'local', 'path' => 'daily/'],
    ['type' => 's3',    'path' => 'daily/'],
], 'gzip');

This dumps once and copies to each storage — cheap, and it is the main reason to use this library over a hand-rolled exec('mysqldump'). Prefer this over calling run() twice.

Schedule it, don't trigger it from a request. Register in routes/console.php (Laravel 11) or Kernel::schedule:

$schedule->command('backup:run')->dailyAt('02:15')->withoutOverlapping();

Always pair with withoutOverlapping() — a slow dump on a large DB will otherwise stack up and fill the temp disk.

Wrap long runs in a queued job when the DB is big or the destination is slow (S3/FTP). The library blocks synchronously, shells out, and uploads; that is a poor fit for HTTP request timeouts:

class RunDatabaseBackup implements ShouldQueue
{
    public $timeout = 3600;

    public function handle(\BackupManager\Manager $manager): void
    {
        $manager->makeBackup()->run('mysql', [
            ['type' => 's3', 'path' => 'nightly/'],
        ], 'gzip');
    }

    public function failed(\Throwable $e): void
    {
        Log::error('Backup job failed', ['message' => $e->getMessage()]);
        // notify Slack/PagerDuty — silent backup failure is the classic outage
    }
}

Restore as an explicit, guarded command. The restore path has the same shape but mirrored arguments:

public function handle(\BackupManager\Manager $manager)
{
    if (! $this->confirm('This will overwrite the current database. Continue?')) {
        return;
    }

    $manager->makeRestore()->run('s3', 'nightly/mysql-2024-01-01.sql.gz', 'mysql', 'gzip');
}

Keep restore permanently wired up in the app. An untested restore path is a backup you don't have; run this command against a staging database at least monthly as part of your ops routine.

Add your own retention pass. The package never deletes anything. Prune in the same scheduled command using the native Storage facade, not the library:

$disk = Storage::disk('s3');
collect($disk->files('nightly'))
    ->filter(fn ($f) => $disk->lastModified($f) < now()->subDays(30)->timestamp)
    ->each(fn ($f) => $disk->delete($f));

Keep environment config in config/backup.php only. Never inline credentials in the provider; the config array is passed straight to Config, so env() in the config file is the right seam. Mirror DB_* env vars rather than duplicating them, so the backup always targets the same DB the app uses.

For staging/local, use local storage and a small retention window so developers can exercise the dump/restore cycle without S3 creds:

'storage' => [
    'local' => ['type' => 'Local', 'root' => storage_path('app/backups')],
],

Gotchas and Tips

The dump binaries must be on the PHP process's PATH. mysqldump, gzip, gunzip are shelled out via Symfony Process. It will work in your SSH shell and fail inside queue workers or PHP-FPM, where PATH is minimal. Debug by logging shell_exec('which mysqldump') from the same context. If the version you have exposes a binary-path config key for the database adapter, set it to the absolute path; otherwise fix PATH in the supervisor/systemd unit.

Flysystem version is the most likely install blocker. The upstream lineage pins Flysystem ~1.x. Laravel 9+ ships Flysystem 3 internally, so composer require will conflict or force a downgrade of unrelated packages. Options: (a) keep this on a Laravel 8 service, (b) vendor src/ and swap the adapter classes for your own thin Flysystem-3 wrappers, or (c) abandon the package and use spatie/laravel-backup, which is maintained and does the same job. Decide this before writing any integration code.

Third-party SDK storage backends may be dead APIs. Dropbox v1, Rackspace Cloud Files, and older AWS SDK v2 are all retired. Treat Dropbox, Rackspace, and AwsS3 as "verify against the live API before relying on it." Local, Ftp, and Sftp (pure PHP/Flysystem or ext-ssh2) are the low-risk adapters. For cloud, local + a separate Storage::disk('s3')->put() sync is a more future-proof design than the built-in S3 adapter.

Dumps are staged on local disk, not streamed. Expect peak disk usage of roughly the uncompressed DB size plus the compressed file. Check storage_path / sys_get_temp_dir() free space in your pre-flight; a full disk yields a truncated dump that uploads "successfully."

Passwords may appear in the process list. If the adapter builds mysqldump -u user -ppassword, any user on the box can read it via ps. Mitigate with a ~/.my.cnf ([client] section, chmod 600) owned by the PHP user, and omit pass from config if the adapter allows it. On shared hosts this is a real risk.

ignoreTables needs fully-qualified names. The value is passed through as --ignore-table, which expects database.table, not table. Passing bare table names silently ignores nothing.

Consistency isn't guaranteed by default. If the adapter doesn't pass --single-transaction for InnoDB, a busy database produces a dump that fails on restore due to FK violations. If there's no config key for extra dump flags, add them in a subclassed MysqlDatabase (see extension points) rather than patching vendor code.

File naming can collide. Names are date-derived; two backups in the same second overwrite each other, and a failed re-run can clobber a good file. If the adapter supports a path suffix, append a unix timestamp or UUID.

Exceptions carry the shell stderr — log the whole thing. Failures surface as BackupManager\ShellProcessing\ShellProcessFailed (name may vary by fork) with the command output in the message. Catch it, log the full message, and alert; a swallowed exception means months of zero backups. Never catch and return true from the scheduled command.

Restore is destructive and unverified. There's no automatic pre-restore snapshot. Before restoring production, always take a fresh manual dump, and restore into a scratch database first. Also confirm compression detection: restoring a .gz file without passing 'gzip' produces a garbage SQL stream that fails midway with a cryptic syntax error.

Empty files are a silent failure mode. Add an assertion after each run — list the destination, confirm a file exists from the last few minutes, and confirm size > a few KB. That single check catches 90% of "backup ran, nothing happened" incidents.

Extension points, if you need to adapt it: implement BackupManager\Filesystems\Filesystem for a new storage target (e.g., Google Cloud Storage) and register it via $filesystems->add(...); implement BackupManager\Databases\Database to add --single-transaction or a different binary; implement the compressor interface for zstd. These are small interfaces — for a 0-star package, subclassing the adapters in app/Support/Backup is preferable to patching vendor/, since you can't count on upstream fixes surviving a composer update.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect