# Backup & restore

Source: https://docs.externa.qiick.io/docs/backup-restore

Disaster-recovery runbook for self-hosted Externa: what to copy, restore order, and downtime expectations. Pair with [Upgrade](/docs/upgrade) before every release bump.

> **Related**
> [Upgrade](/docs/upgrade) · [Deployment](/docs/deployment) · [Files configuration](/docs/files-configuration) · [Supported databases](/docs/supported-databases)

## 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

```bash
pg_dump -Fc -h "$DB_HOST" -U "$DB_USERNAME" -d "$DB_DATABASE" -f externa-$(date +%F).dump
```

### MySQL / MariaDB

```bash
mysqldump -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" \
  --single-transaction --routines --triggers "$DB_DATABASE" \
  > externa-$(date +%F).sql
```

### SQLite

```bash
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

```bash
# 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.

```bash
./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`.

```bash
# 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

1. Provision empty host / containers; restore `.env` (fix secrets if the incident was a breach).
2. Restore **database** first.
3. Restore **`storage/app`** (and link `php artisan storage:link`).
4. `composer install` / image rebuild for the **same** app version as the dump.
5. `php artisan migrate --force` only if upgrading past the dump’s schema — otherwise skip.
6. `php artisan optimize:clear`; restart workers.
7. Smoke: `/health/ready`, login, one collection, one file download.

### Postgres restore

```bash
pg_restore -h "$DB_HOST" -U "$DB_USERNAME" -d "$DB_DATABASE" --clean --if-exists externa-YYYY-MM-DD.dump
```

### MySQL restore

```bash
mysql -h "$DB_HOST" -u "$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < externa-YYYY-MM-DD.sql
```

### SQLite restore

```bash
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.
