Skip to content

Backups and restore

Capell can create versioned database and media snapshots, verify their health, apply retention, and prove recovery in isolated scratch targets. Backups are disabled by default and never restore over live data.

This capability protects Capell content. Keep separate infrastructure backups for .env, APP_KEY, composer.lock, deploy configuration, logs, and any files not stored on configured media disks.

Configure a private Laravel filesystem disk that is different from every media source disk. Object storage with independent versioning and lifecycle controls is recommended for production.

CAPELL_BACKUP_ENABLED=true
CAPELL_BACKUP_DISK=backups
CAPELL_BACKUP_PREFIX=capell-backups
CAPELL_BACKUP_DB_CONNECTION=mysql
CAPELL_BACKUP_MEDIA_DISKS=public,media
CAPELL_BACKUP_MAX_AGE_HOURS=26
CAPELL_BACKUP_MINIMUM_RETAINED=7
CAPELL_BACKUP_RETAIN=30
CAPELL_BACKUP_PRUNE_SCHEDULE_ENABLED=false
CAPELL_BACKUP_PRUNE_SCHEDULE_CRON="0 3 * * 1"
CAPELL_BACKUP_PROCESS_TIMEOUT_SECONDS=3600
CAPELL_BACKUP_SCRATCH_DATABASE_PREFIX=capell_restore_
CAPELL_BACKUP_SCRATCH_SQLITE_DIRECTORY=/srv/capell/restore-scratch

The configured database connection may use SQLite, MySQL/MariaDB, or PostgreSQL. MySQL/MariaDB hosts need mysqldump and mysql; PostgreSQL hosts need pg_dump and psql. Override binary paths with CAPELL_BACKUP_MYSQLDUMP_BINARY, CAPELL_BACKUP_MYSQL_BINARY, CAPELL_BACKUP_PG_DUMP_BINARY, and CAPELL_BACKUP_PSQL_BINARY when needed. Increase CAPELL_BACKUP_PROCESS_TIMEOUT_SECONDS when a large database cannot complete within one hour.

Each completed snapshot contains a compressed database artifact, configured media files, and a manifest written last. The manifest records sizes and SHA-256 checksums, but no database password, file content, or absolute source path. A prefix without manifest.json is incomplete and cannot be restored or automatically pruned.

Checksums detect corruption; they are not signatures. Restrict write access to the backup disk, enable provider-side versioning where available, and monitor storage audit logs so an attacker cannot replace both a manifest and artifacts.

Terminal window
php artisan capell:backup:create
php artisan capell:backup:create --database-only
php artisan capell:backup:health
php artisan capell:backup:health --json
php artisan capell:backup:prune
php artisan capell:backup:prune --force

Health returns a non-zero exit code when storage is unavailable, there is no completed snapshot, the newest snapshot is stale, retention is insufficient, or a manifest/artifact size or checksum is wrong. Connect the JSON form to the normal monitoring system.

Pruning is a dry run unless --force is supplied. It keeps the newest CAPELL_BACKUP_RETAIN completed snapshots and refuses unsafe identifiers, out-of-prefix paths, malformed manifests, and incomplete snapshots.

Capell can schedule forced pruning when CAPELL_BACKUP_PRUNE_SCHEDULE_ENABLED=true. It is disabled by default because pruning deletes completed snapshots. The default cron runs at 03:00 every Monday; override it with CAPELL_BACKUP_PRUNE_SCHEDULE_CRON. Scheduled pruning uses Laravel’s overlap and single-server guards.

Schedule creation before the health check. If Capell’s built-in pruning schedule remains disabled, schedule pruning separately:

use Illuminate\Support\Facades\Schedule;
Schedule::command('capell:backup:create')->dailyAt('01:00');
Schedule::command('capell:backup:health --json')->dailyAt('02:00');
Schedule::command('capell:backup:prune --force')->weeklyOn(1, '03:00');

Choose a new database name with the configured scratch prefix. When the snapshot contains media, choose a non-live disk and an empty non-live prefix.

Terminal window
php artisan capell:backup:restore \
20260710T010000Z-a1b2c3d4e5f6 \
capell_restore_20260710 \
--media-disk=restore-scratch \
--media-prefix=drills/20260710

Before mutation, restore validates the manifest identity and every artifact’s path, size, and checksum. It then restores the database and media into scratch targets and runs capell:doctor --json in a child process against that scratch database. Passwords remain in process environments for database tools and are never included in command arguments or output.

Single-disk snapshots retain the existing prefix/source-path restore layout. Multi-disk snapshots restore to prefix/source-disk/source-path, with each disk name URL-encoded as one directory segment. Destination collisions (including case-only, equivalent Unicode spellings, and file/directory conflicts) and unsafe paths are rejected before database or media mutation. A scratch prefix beneath an existing file is also rejected before the database restore starts. Completed transfer temporary files are released as soon as their write and checksum work finishes, so media scratch usage is bounded by the active artifact rather than the entire collection.

The command deliberately cannot restore over a live database or media disk. If doctor verification fails, scratch data is left in place for diagnosis; remove it manually after investigating. A successful monthly drill should be recorded with its snapshot ID, completion time, doctor result, and cleanup.

During an incident, first preserve the failed environment and verify the selected snapshot with a scratch restore. Promoting recovered data into production remains an operator-controlled infrastructure procedure: take the application offline, stop workers, use the database platform’s supported promotion/import workflow, restore media with the storage provider’s tooling, clear caches, restart workers, and rerun Site Health.

Capell does not automate in-place production recovery because overwriting live data is a high-impact, platform-specific operation. Keep .env, APP_KEY, deployment state, and non-media files in the infrastructure backup plan, and test that plan alongside this snapshot drill.

Assume an operator confirms database corruption at 09:42 and the newest healthy snapshot completed at 09:00.

  1. Record the incident time, affected release, database host, newest healthy snapshot ID, and recovery lead. Enable maintenance or lockdown and stop queue workers so writes do not continue.

  2. Preserve the failed database and current media state. Do not overwrite the only evidence while trying to recover quickly.

  3. Verify backup health, then restore into isolated scratch targets:

    Terminal window
    php artisan capell:backup:health --json
    php artisan capell:backup:restore \
    20260712T090000Z-a1b2c3d4e5f6 \
    capell_restore_incident_20260712 \
    --media-disk=restore-scratch \
    --media-prefix=incidents/20260712
  4. Review the doctor result. On the scratch copy, compare critical record counts, open representative pages and media, and confirm that the accepted recovery point loses no more data than the incident decision allows.

  5. Use the database and storage provider’s documented promotion/import process to replace production from the verified result. This is intentionally outside Capell and needs the platform’s rollback mechanism.

  6. Deploy the matching application release, run migrations only when required, then clear caches and restart workers:

    Terminal window
    php artisan optimize:clear
    php artisan queue:restart
    php artisan capell:doctor --json
    php artisan capell:backup:health --json
  7. Verify signed-out pages, admin login, publishing, media, redirects, scheduled work, and Marketplace connectivity before reopening traffic. Record the recovery point, lost-write window, evidence, and cleanup owner.

If verification fails, keep traffic closed and roll back through the infrastructure provider. Do not improvise an in-place database merge during an incident unless that procedure was already rehearsed.