rupadana/remote-database-backup
A Laravel package for managing remote database backups. Configure your database connections and storage destinations, then run backups manually or on a schedule to keep copies of your data safely off-server.
Install and publish
composer require rupadana/remote-database-backup
php artisan vendor:publish --tag="remote-database-backup-config"
Check the tag name against the README before running it — publish tags are inconsistent across first releases, and a wrong tag silently does nothing. If the package ships migrations (e.g. a backup/run history table), publish and run those in the same sitting so you don't discover a missing table at 02:00 when the scheduler fires.
Where to look first, in this order
config/remote-database-backup.php — this is the real API surface. Source connections, destination disks, retention, and binary paths will all live here. Read it top to bottom before touching anything else.php artisan list | grep -i backup — confirm the actual command signatures rather than trusting the README. On a 1.0.0 tag the docs and the code are sometimes one commit apart.vendor/rupadana/remote-database-backup/src/ — it's a small package. Ten minutes of reading saves an hour of guessing, especially given the release notes here carry no changelog content at all.First use case to wire up
The canonical one: dump a remote/production database and land the artifact on a configured filesystem disk (S3 or local), on a schedule.
// config/remote-database-backup.php
'connections' => [
'production' => [
'driver' => 'mysql',
'host' => env('PROD_DB_HOST'),
'database' => env('PROD_DB_DATABASE'),
'username' => env('PROD_DB_USERNAME'),
'password' => env('PROD_DB_PASSWORD'),
],
],
'disks' => ['s3'],
'retention' => [
'days' => 14,
'count' => 20,
],
Then schedule it:
Schedule::command('remote-database-backup:run')->dailyAt('01:30');
Do this once, end to end, against a throwaway database before pointing it at production. The failure modes below are unglamorous but guaranteed.
Config-driven, not call-driven. Define each source database as a named entry in the config and select it by key at call time. This keeps credentials in env() where they belong and lets staging/production differ by environment rather than by code branch.
Schedule::command('remote-database-backup:run --connection=production')->daily();
Schedule::command('remote-database-backup:run --connection=billing')->weekly();
Wrap scheduled runs defensively. Backups are I/O-bound and variable in duration; a slow night must not stack runs.
Schedule::command('remote-database-backup:run')
->dailyAt('01:30')
->withoutOverlapping()
->onOneServer()
->runInBackground();
Retention as config, not as a cron job. Express retention declaratively (age and/or count) and let the package prune. Hand-rolled find -mtime pruning is where people delete the wrong directory.
Separate the backup tier from the app tier. Run backups from a dedicated worker or a small scheduler instance rather than a web node. This isolates the CPU/disk cost of compression and upload, and avoids coupling backup success to app deploy churn.
Notifications on failure, at minimum. Wire the package's events or Laravel's Notification::route(...) for failures. A backup system you only hear from on success is a backup system you stop reading. Alert on absence too — a heartbeat check (Healthchecks.io style) catches the case where the scheduler itself silently dies.
Pull-to-local for dev refresh. The highest day-to-day value of a package like this is often the inverse direction: grab a recent anonymised snapshot into a developer's local environment. If the package supports reading from a disk as well as writing, wrap it in a php artisan db:refresh-local project command and gate it so it can never target a non-local host. Be deliberate about PII — a raw production dump on a laptop is a data protection incident waiting to be filed.
Test with Storage::fake(). Assert the artifact exists and is non-empty, not just that the command exited 0.
Storage::fake('backups');
$this->artisan('remote-database-backup:run')->assertSuccessful();
Storage::disk('backups')->assertExists(
Storage::disk('backups')->allFiles()[0]
);
Also assert the non-trivial cases: remote host unreachable, disk full, wrong credentials. Those are the branches that matter.
1.0.0 with an empty changelog is a signal, not a footnote. The release notes here are a bare link to the commit range — no breaking changes, no features, no deprecations listed. Treat the version as "API stabilised, documentation still catching up." Pin ^1.0, read src/ before adopting, and re-diff on every minor bump until the project demonstrates a changelog discipline. Do not assume a 0.x → 1.0.0 jump carried a migration guide; if you were on a dev branch, diff the config file by hand.
Dump binaries are a remote-host dependency. mysqldump, pg_dump, or sqlite3 must exist on whichever machine actually executes the dump — and be on the PATH of the non-interactive shell user. This is the single most common first-run failure: works over SSH interactively, fails from cron because PATH differs. Pin absolute binary paths in config if the package allows it.
SSH authentication. Key-based auth only; password auth via SSH from a PHP process is fragile and usually disabled server-side anyway. Ensure the deploy user's key is the one being used (agent forwarding does not exist in a cron context), and decide deliberately whether to disable host key checking. Enabling StrictHostKeyChecking is correct but means the remote fingerprint must be pre-seeded in known_hosts.
Timeouts stack, and they will bite you. There are at least four: PHP max_execution_time, the process timeout in the scheduler, the SSH connection timeout, and the underlying dump's own timeout. For anything above a few hundred MB, run it as a queued job rather than inline in the scheduler, and set a generous retry_after on the queue connection so the job isn't killed mid-upload.
Compression is where large databases break. gzip at default settings on a multi-GB dump will saturate a core for a long time. Prefer streaming (mysqldump | gzip > file) over writing an uncompressed dump to disk and compressing afterwards — the latter needs 2–3× the database size in free temp space, which is exactly the disk nobody monitors.
Credentials leak into logs more often than you'd think. Before enabling debug verbosity, confirm the package redacts passwords in exception messages and command output. If it shells out, the full command line — including a -p<password> — may land in your logs or in ps output. Prefer --defaults-extra-file / PGPASSFILE style auth where supported.
Verify restores, not backups. A backup you have never restored is a hypothesis. Put a quarterly restore drill on the calendar, and make it a real restore into a scratch database followed by a row-count sanity check. If the package's local-dev refresh command works, it doubles as your restore test — a nice side effect worth designing for.
Debugging a failed run. In order: php artisan config:clear (stale config after editing the published file is a classic), then run the command manually with -vvv as the same user the scheduler uses (sudo -u www-data php artisan ...), then reproduce the underlying dump command by hand over SSH. Isolating whether the failure is connection, dump, compress, or upload collapses the search space immediately.
Extension points to check for. Look for a driver/connection resolver interface, pre/post-backup events, and a filesystem-disk abstraction. If destinations go through Laravel's Storage facade, you get S3, GCS, SFTP, and local for free and can add a custom adapter without forking. If they're hardcoded to local paths, that's the first thing to evaluate before committing — it's the constraint most likely to force a fork later.
How can I help you explore Laravel packages today?