Zodiac Tech CRM Docs

Backups & restore

The built-in leadhub:backup command, the nightly schedule toggle, what the archive contains, manual cPanel/mysqldump backups, and how to restore safely.

super-admin developer support Medium ⏱ 20 min Updated 2026-10-05

Zodiac Tech CRM ships a native backup system — no extra packages, no Redis, no cloud account. One command produces a single zip containing the full database and all uploaded files. This page covers running it, scheduling it, restoring from it, and the cPanel-side belt-and-braces.

Who this is for

  • Super-admin / hosting admin — owning the backup routine.
  • Support — performing a restore during an incident.

Prerequisites: the app is installed; Terminal access makes life easier (the Super Admin UI covers the rest).

What a backup contains

leadhub:backup creates storage/app/backups/leadhub_backup_YYYYMMDD_HHMMSS.zip containing:

Entry Content
database.sql Full database dump — the mysqldump binary when reachable (fast, includes routines/triggers/events), otherwise a pure-PHP PDO dump (table-only; works where exec() is disabled)
storage/public/** Uploaded public files (branding assets, tenant branding, WhatsApp/Meta media)
storage/private/** Call recordings (recordings/{tenant}/{call}/**) and support attachments — added 2026-10-06; archives from earlier versions do not contain this entry
manifest.json created_at, app version, Laravel/PHP versions, which parts were included

It does not include your .env or the application code — keep those separately (.env in your password manager; code in your release zip/git).

Step 1 — Take a backup now

cd ~/crm-app
php artisan leadhub:backup

Or via the UI: Super Admin → Backups → Create backup.

Expected result: a line like Backup created: leadhub_backup_20261005_020000.zip (48.2 MB) and the archive listed on the Backups page.

Step 2 — Verify the archive

The Backups page can smoke-test an archive without restoring it (checks the zip is well-formed, database.sql exists with content, and roughly counts SQL statements). Do this at least once so you know your backups are restorable before the day you need one.

First rule of backups A backup you have never restored is a hope, not a backup. Run one restore drill on a staging copy.

Step 3 — Schedule it (nightly toggle)

  1. Super Admin → Script Settings → enable Automatic nightly backup (auto_nightly_backup).
  2. The scheduler already contains the job: leadhub:backup runs daily at 02:00, and only executes when either that toggle or config('leadhub.backups.auto_nightly') (LEADHUB_AUTO_BACKUP env) is on.
  3. Ensure your cron entry is live — without it the 02:00 job never fires.

With Terminal, add pruning so disk doesn't fill:

php artisan leadhub:backup --prune=14

--prune=14 deletes backups older than 14 days after the new one is written. The scheduled task runs without --prune, so decide your retention policy consciously (see below).

Step 4 — Retention advice

  • Daily backups: keep 7–14 on the server (--prune=7 / --prune=14 for a manual cron, or delete from the Backups page).
  • Off-site: download the latest zip weekly (Backups page → download, or scp/FTP from storage/app/backups/). A server failure takes server-side backups with it.
  • Before every update: take one manual backup — see Updates & rollback.
  • The download action streams the archive; on slow connections use FTP for large files.

Step 5 — Restore from a backup

In-app restore (recommended):

  1. Super Admin → Backups.
  2. Find the archive → Restore.
  3. The service validates the zip (zip-slip protection), extracts storage/public/* back into storage/app/public/ and storage/private/* back into storage/app/private/ (recordings, support attachments), then imports database.sql statement-by-statement (streamed — memory-safe even for large dumps).
  4. You get a summary: files restored + rows/statements imported.

Restore is destructive The SQL dump contains DROP TABLE/CREATE TABLE statements — restoring replaces current data with the archive's contents. Restore during a maintenance window, and take a fresh backup of the current state first if it matters.

Manual restore (alternative):

# On the server
cd ~/crm-app
unzip storage/app/backups/leadhub_backup_20261005_020000.zip -d /tmp/restore
# 1) files
cp -r /tmp/restore/storage/public/. storage/app/public/
cp -r /tmp/restore/storage/private/. storage/app/private/   # recordings + attachments
# 2) database
mysql -u youruser -p youruser_leadhub < /tmp/restore/database.sql

Via phpMyAdmin instead of CLI: create/select the database → Import → choose database.sql.

After any restore:

php artisan optimize:clear
php artisan queue:restart

cPanel-side extras

  • Backup Wizard / Backup (cPanel): full-account backups include files + databases. Good as a monthly safety net; clumsy for single-table recovery.

  • phpMyAdmin → Export: quick ad-hoc DB export (choose utf8mb4).

  • File Manager → Compress: zip storage/app/public before big changes.

  • mysqldump by hand:

    mysqldump -u youruser -p --no-tablespaces --single-transaction youruser_leadhub > leadhub.sql
    

    (The product's own dump uses --no-tablespaces --single-transaction --quick --lock-tables=false --routines --triggers --events.)

What backups don't cover .env (secrets) and application code. After a full server loss you need: this zip plus the release package/code and your .env values. Store the latter two in separate places.

Common mistakes

  • Enabling the nightly toggle but never checking the cron runs — the 02:00 job silently never fires.
  • --prune smaller than your off-site download cadence, so the only copy ages out.
  • Backing up while a huge export is running (large storage churn); fine in practice, but note the zip is not a point-in-time snapshot of uploads.
  • Restoring an archive from a different version of the app without running migrations afterward — schema mismatch. Restore matching versions; migrate after upgrading.
  • Testing restores only on production.

Troubleshooting

Symptom Check
only_mysql_supported error The backup service supports MySQL only — check DB_CONNECTION
Backup fails with zip error Disk full or storage/app/backups not writable
Restore imports 0 statements Archive's database.sql corrupt/empty — use the verify action on the latest archive and fall back to the next one
Restore "backup not found" File name must match leadhub_backup_YYYYMMDD_HHMMSS.zip and live in storage/app/backups/
Nightly job logged as failed Check schedule task failed: leadhub:backup in laravel.log; usually permissions or disk

Technical details

  • Command: App\Console\Commands\RunBackup (leadhub:backup {--prune=}, BackupService::create(['database'=>true,'files'=>true])).
  • Service: App\Services\BackupService — mysqldump→PDO fallback, zip-slip-validated restore, streamed SQL import, verify() smoke test.
  • Schedule: routes/console.php — daily 02:00, gated by GeneralSettings->auto_nightly_backup || config('leadhub.backups.auto_nightly').
  • UI: App\Filament\SuperAdmin\Pages\Backups; toggle in App\Filament\SuperAdmin\Pages\ScriptSettings.
  • Archive location: storage/app/backups/.

Was this page helpful?