spatie/laravel-db-snapshots
Spatie's Laravel package adding Artisan commands to instantly dump and load MySQL, PostgreSQL, or SQLite databases. Create named snapshots, load the latest, list, and clean up old dumps in seconds.
Install the package, register a disk, then run a snapshot in under five minutes:
composer require spatie/laravel-db-snapshots
Add a snapshots disk in config/filesystems.php — this is the one required configuration step. If the disk doesn't exist, every command fails:
'disks' => [
'snapshots' => [
'driver' => 'local',
'root' => database_path('snapshots'),
],
],
Then create, mutate, and restore:
php artisan snapshot:create before-import
# ... run a migration, import, or fuzz the DB ...
php artisan snapshot:load before-import
First things to read in the repo: the README's Usage section (the actual surface area is ~8 artisan commands), the published config/db-snapshots.php, and src/Events/* if you plan to hook into the lifecycle. The package is a thin wrapper around spatie/db-dumper, so anything the underlying mysqldump/pg_dump/sqlite3 supports is reachable via the dump.addExtraOption connection key.
Publish the config as soon as you go beyond local dev:
php artisan vendor:publish --provider="Spatie\DbSnapshots\DbSnapshotsServiceProvider"
First real use case: seed a known-good local state before a risky migration, then reload it when the migration misbehaves. The second is snapshot:load --latest as a "reset to yesterday" button in staging.
Point the disk at cloud storage for shared/shared-stage snapshots. Because snapshots go through Laravel's filesystem abstraction, swapping local for s3 gives you a team-shared snapshot store with zero code changes. Keep the temp directory on fast local disk (temporary_directory_path), though — the dump is written there first and then moved to the disk.
Compress by default in config, not per-command. 'compress' => true in the config means every snapshot is .sql.gz, which matters a lot once dumps exceed a few hundred MB. Remember to use --stream when loading large compressed files:
php artisan snapshot:load --latest --stream
Restore into a different connection for safe inspection. A very common workflow is dumping production-ish data and restoring it into a scratch connection rather than blowing away your dev DB:
php artisan snapshot:create prod-readonly --connection=production
php artisan snapshot:load prod-readonly_2026-01-15 --connection=scratch --drop-tables=1
Note the naming quirk: passing --connection without a name prepends the connection to an auto-generated timestamp name (logging_2017-03-17 14:31), so snapshot:list output is self-documenting. If you supply a name, no prefix is added.
Table-scoped snapshots for fixtures. --table and --exclude accept repeats or comma lists. Use --table for a small fixture set (reference data, CMS seed content) and --exclude when you want everything except huge log/telescope/jobs tables:
php artisan snapshot:create reference-data --table=countries,currencies,tax_rates
php artisan snapshot:create weekly --exclude=telescope_entries,jobs,failed_jobs,sessions
If both are passed, --table wins and --exclude is silently ignored — deliberately never pass both.
Schedule cleanups and creations. snapshot:cleanup --keep=N is a natural fit for the scheduler:
$schedule->command('snapshot:create nightly --compress')->dailyAt('02:00');
$schedule->command('snapshot:cleanup --keep=7')->dailyAt('02:30');
This gives you a rolling 7-day retention without writing a single line of backup logic.
Hook lifecycle events for observability. All six events carry the snapshot object, so you can log to Slack, push a metric, or copy the file off-disk after creation:
Event::listen(CreatedSnapshot::class, function ($event) {
Log::info('snapshot created', ['name' => $event->snapshot->name]);
});
CreatingSnapshot / LoadingSnapshot are also useful places to abort (throw) if, say, the target env is production.
CI / test integration. The classic pattern is to check a small snapshot into the repo (or pull it in CI) and load it in a test bootstrap or a Pest beforeAll, replacing slow factory seeding for integration suites. Use --drop-tables=0 when loading into a DB where you want to preserve schema and only insert data — useful when the target DB is a shared test instance and dropping tables would break other suites.
Extra dumper flags via the connection config. Postgres restores commonly need ownership/privilege adjustments:
'pgsql' => [
// ...
'dump' => [
'addExtraOption' => '--no-owner',
],
],
snapshots disk must exist before any command runs. This is the #1 first-run failure. If you renamed the disk, also update db-snapshots.disk.snapshot:load drops all tables by default. That's destructive on the target connection. Guard scripts, and use --drop-tables=0 when you only want data merged in.--stream, the whole dump is read into memory. If a load dies with an OOM, --stream is the fix — it was hardened in 2.9.0 for chunk-boundary and continued-line cases, so stay on a recent 2.x.PATH. mysqldump, pg_dump/psql, sqlite3. Version mismatches between client and server (especially pg_dump older than the server) produce confusing errors that look like package bugs.psql \ commands in dumps were skipped starting 2.7.2 — if you're on an older version and see invalid command \... errors on load, upgrade rather than editing the dump.db-snapshots.php or the connection dump array, run php artisan config:clear in local dev. In production, re-cache.storage/app/laravel-db-snapshots/temp must be writable; in containerized deploys storage is often a read-only or ephemeral mount.2017-03-17 14:31). Quote them in shell scripts, or always pass explicit kebab-case names in automation.--latest is lexical/sort-order based on names, not a hidden "newest" index. If you mix custom names with date names, "latest" can surprise you (e.g. zzz-test beats a timestamp). Prefer dated prefixes for automated snapshots.--table + --exclude together silently ignores --exclude. Validate in your own wrapper scripts if this matters.dump.addExtraOption is essentially the whole API. If you need post-dump transforms (scrubbing PII, rewriting DEFINER clauses), listen to CreatedSnapshot and post-process the file on the disk — or wrap the artisan command in your own command.How can I help you explore Laravel packages today?