A practical backup system using mysqldump, cron, and offsite rotation — plus the disaster recovery plan we actually tested by destroying a staging server.
The Backup Rule Nobody Follows
The 3-2-1 backup rule says: keep 3 copies of your data, on 2 different storage types, with 1 copy offsite. Most developers know this rule. Almost nobody follows it for their CMS installations. They rely on the hosting provider's daily backup and hope for the best.
That approach holds until a hosting migration goes wrong and days of content disappear — the classic surprise being that the provider's "daily backup" turns out to be three days old. Owning your backup strategy is the only reliable fix.
How jekcms Actually Backs Up Your Site
jekcms's BackupManager class produces a single .zip file per backup, containing db.sql (the database dump), the full uploads/ directory, and a manifest.json with metadata — version, post count, and a schema digest. There's no separate database-only and files-only backup split; every backup is a complete, self-contained snapshot.
The database dump itself is implemented in pure PHP rather than shelling out to mysqldump — deliberately, because mysqldump isn't reliably available on shared hosting. The dump streams rows one table at a time instead of loading full result sets into memory, so it holds up on installations with large post tables. The only hard requirement is the PHP zip extension; BackupManager throws immediately if ZipArchive isn't available rather than producing a silently broken backup.
// Simplified real shape of BackupManager
class BackupManager
{
public function create(string $type = 'full', string $trigger = 'manual'): array { /* ... */ }
public function list(int $limit = 20): array { /* ... */ }
public function get(int $id): ?array { /* ... */ }
public function restore(string $zipPath): array { /* ... */ }
public function scheduledRun(): array { /* ... */ }
// dumpDatabase() is private — pure-PHP mysqldump replacement
}
Every backup is recorded in a backups table, which is what list() and the admin panel's backup history read from. The backups directory itself gets a .htaccess deny-all written automatically, so backup archives aren't reachable over HTTP even if someone guesses the path.
Scheduling and Retention
Scheduled backups are controlled by a handful of settings: whether scheduling is enabled at all, what hour they run (default 3 AM, configurable), and a retention window (default 30 days, clamped between 7 and 365). scheduledRun() is idempotent — calling it twice in the same day is a no-op if a backup already ran — and after a scheduled backup completes, it prunes backups older than the retention window automatically. There's no separate daily/weekly/monthly rotation tier to configure; it's one schedule, one retention period, kept simple on purpose.
Testing Recovery
A backup you have never tested is not a backup — it is a hope. restore(string $zipPath) is the same code path whether you're recovering from an accident or verifying a backup is actually usable. Periodically restoring to a staging copy of your site is the only way to know your backups would actually save you, rather than assuming they would.