Skip to content

Backups & disaster recovery

Imaginook backs itself up with two Artisan commands and prunes by retention. This page covers what is backed up, how to restore, the volume-snapshot layer, and the recurring restore drill.

What's backed up

imaginook:backup (scheduled nightly at 02:30) writes a set to the backup disk (config('imaginook.backup.disk'), local by default; set BACKUP_DISK=s3 for off-site object storage):

  • database.sql.gz — a pg_dump of the whole database (--clean --if-exists, so it restores into a fresh cluster).
  • sites.tar.gz — the user-site trees (sites + buttons + avatars disks).

When BACKUP_ENCRYPTION_KEY is set, every archive is encrypted at rest with openssl (AES-256); files then end in .enc. Sets older than BACKUP_RETENTION_DAYS (default 14) are pruned automatically, and each run writes a .last-backup.json health manifest that the admin diagnostics panel shows. A failed run raises a backup.failed audit event, which fans out to the outbound webhook so you hear about it.

Off-site + snapshots (defence in depth)

The nightly logical backup is the primary recovery path. For a hosted instance, layer on:

  1. Off-site archives — point BACKUP_DISK at an S3-compatible bucket in a different region/provider than the host.
  2. Block-storage snapshots — enable your provider's volume snapshots for the Postgres data volume and the user-site volume, on a schedule at least as frequent as the backup (e.g. daily, retained 7–30 days). Snapshots give a fast, crash-consistent rollback; the logical dump gives a portable, verifiable, cross-version restore. Keep both.

Restoring

A restore replaces the database and site trees. Take a fresh snapshot first.

bash
# List sets and confirm the newest is restorable — non-destructive (a DR drill):
php artisan imaginook:restore --latest --verify

# Restore a specific set (destructive; requires --force):
php artisan imaginook:restore --set=20260711-023000 --force

Restore fetches the set from the backup disk, decrypts it (needs the same BACKUP_ENCRYPTION_KEY), verifies the dump's gzip integrity, applies it with psql -v ON_ERROR_STOP=1, and extracts each site tree back into its configured root. After a restore, rebuild the nginx host map and caches:

bash
php artisan imaginook:nginx-sync      # rebuild host→webroot maps from the DB
php artisan config:cache && php artisan route:cache

The restore drill

Backups you have never restored are not backups. Run the drill on a schedule (monthly is a good default) and record the result:

  1. php artisan imaginook:restore --latest --verify — confirms the newest set is present, decryptable and gzip-intact without touching production.
  2. Once a quarter, do a real restore into a throwaway instance (a scratch VM or the all-in-one image with a copy of the set) and smoke-test /up, /readyz, and a couple of sites. Note the wall-clock time to restore.
  3. Log the drill date, set label, and outcome in your ops record.

If --verify ever fails, treat it as an incident: the most recent backups are not recoverable, and you need to fix the backup pipeline before you need it for real.

AGPL-3.0 · built in the open.