Operations
Backup & restore
Disaster-recovery runbook for self-hosted Externa: what to copy, restore order, and downtime expectations. Pair with Upgrade before every release bump.
Related
What to back up
| Asset | Required? | Notes |
|---|---|---|
| Database | Yes | All CMS data, users, jobs tables |
.env | Yes | Secrets + config — store separately from public backups |
storage/ | Yes (local disks) | Uploads, zips, logs, framework files under storage/app |
| S3 / MinIO objects | If FILES_DISK=s3 | Objects live in the bucket — use bucket versioning / lifecycle; still backup DB + .env |
| Code tree | Optional | Prefer Git tag / Packagist release over tarring vendor/ |
Downtime: DB dump is consistent if you freeze writes (php artisan down or brief maintenance). File copy can run warm; restore expects matching DB + files.
Backup — databases
PostgreSQL
pg_dump -Fc -h "$DB_HOST" -U "$DB_USERNAME" -d "$DB_DATABASE" -f externa-$(date +%F).dump
MySQL / MariaDB
mysqldump -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" \
--single-transaction --routines --triggers "$DB_DATABASE" \
> externa-$(date +%F).sql
SQLite
php artisan down
cp database/database.sqlite "backups/database-$(date +%F).sqlite"
# also copy WAL/SHM if present while down
php artisan up
Backup — files and env
# from project root
tar -czf "backups/storage-$(date +%F).tar.gz" storage/app storage/logs
cp .env "backups/env-$(date +%F).env" # keep offline / encrypted
Never commit backup tarballs or .env copies to git.
Optional script
Repo helper (core): scripts/backup.sh — dumps DB (when CLI tools exist) + tars storage/app into ./backups/ (gitignored). Review before cron.
./scripts/backup.sh
Docker Compose volumes
Local: externa_pgsql, externa_storage, … — see compose.yaml volumes:.
Prod: externa_prod_pgsql, externa_prod_storage, externa_prod_redis.
# example: snapshot Postgres volume via running service
docker compose -f compose.prod.yaml exec -T pgsql \
pg_dump -U externa -d externa -Fc > externa-prod-$(date +%F).dump
# storage volume → tar from app container
docker compose -f compose.prod.yaml exec -T app \
tar -czf - -C /var/www/html storage/app > storage-$(date +%F).tar.gz
Restore order
- Provision empty host / containers; restore
.env(fix secrets if the incident was a breach). - Restore database first.
- Restore
storage/app(and linkphp artisan storage:link). composer install/ image rebuild for the same app version as the dump.php artisan migrate --forceonly if upgrading past the dump’s schema — otherwise skip.php artisan optimize:clear; restart workers.- Smoke:
/health/ready, login, one collection, one file download.
Postgres restore
pg_restore -h "$DB_HOST" -U "$DB_USERNAME" -d "$DB_DATABASE" --clean --if-exists externa-YYYY-MM-DD.dump
MySQL restore
mysql -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < externa-YYYY-MM-DD.sql
SQLite restore
php artisan down
cp backups/database-YYYY-MM-DD.sqlite database/database.sqlite
php artisan up
S3 caveat
With FILES_DISK=s3, DB rows point at object keys. Backup the bucket (or rely on provider versioning) separately from the DB dump. Restoring an old DB against a newer bucket (or vice versa) yields missing objects.
Dry-run note
Operators should practice restore once on a non-prod copy before relying on backups for GA. Record date/host in your runbook when you do.