laravel-database-copy maintained by sextanet
Laravel Database Copy
Copy a database between environments (e.g. production → staging) through a disk such as S3. The copy is anonymized before it leaves the source server, so the real data never reaches the bucket nor the other environment.
Production ──database-copy:export──▶ dump → anonymization database (same server) → anonymize → dump → encrypt
│
disk: {environment}/{name}/{Y-m-d-H-i-s}.zip
│
Staging ◀──database-copy:import── decrypt → check it is anonymized → replace database → migrate ┘
Installation
composer require sextanet/laravel-database-copy
php artisan vendor:publish --tag="database-copy-config"
DATABASE_COPY_DISK=s3
DATABASE_COPY_PASSWORD= # the SAME in every environment
DATABASE_COPY_NAME=my-app # folder of this app in the disk (default: slug of APP_NAME)
# Source environment only
DATABASE_COPY_ANONYMIZATION_DATABASE= # default: {database}_anonymized
DATABASE_COPY_EMAIL_DOMAIN=anonymized.test
DATABASE_COPY_KEEP_EMAILS=seba@sextanet.cl # rows that keep their data, comma separated
The source environment needs mysqldump/mysql (or sqlite3, pg_dump/psql) and an empty anonymization
database on the same server that the database user can write to:
CREATE DATABASE my_app_anonymized;
GRANT ALL PRIVILEGES ON my_app_anonymized.* TO 'my_app'@'localhost';
It is emptied after every export, even when something fails. With SQLite there is nothing to create: the file
(database/database_anonymized.sqlite by default) is created and deleted on every export.
Anonymization
config/database-copy.php is the source of truth of what is personal data. Add every table or column with personal
data there:
'tables' => [
'users' => [
'name' => 'first_name',
'last_name' => 'last_name',
'rut' => 'rut',
'email' => 'email',
'phone' => 'phone',
'birthday' => 'null',
'password' => 'password',
'remember_token' => 'null',
],
'comments' => [
'email' => 'email',
'ip' => 'ip',
'user_agent' => 'user_agent',
'address' => 'address', // any Faker formatter
],
],
'truncate' => ['sessions', 'password_reset_tokens', 'personal_access_tokens', 'jobs', 'failed_jobs'],
Tables or columns that do not exist are skipped. Anonymized tables need an id column.
Usage
# Source (production): upload an anonymized copy
php artisan database-copy:export
# Target (staging, local): replace this database with the latest copy from production
php artisan database-copy:import
php artisan database-copy:import --from=staging
php artisan database-copy:import --file=production/my-app/2026-10-04-03-30-00.zip
Copies are named with the date of the source app in its timezone (config('app.timezone'), UTC by default).
Schedule the export in the source environment:
Schedule::command('database-copy:export')->dailyAt('03:30');
Safety:
database-copy:importnever runs inprotected_environments(productionby default) and asks for confirmation (unless--force).- It refuses a copy that is not anonymized (
database-copy:export --without-anonymization) unless--with-real-data. - The copy is decrypted and checked before the database is touched.
- The export refuses to anonymize when the anonymization database is the source database.
Testing
composer test
Credits
License
The MIT License (MIT). Please see License File for more information.