Backup
The Backup admin page (/app/admin/backup) manages full server backups containing a MySQL database dump, config files, user data, and SSL certificates. Backups can be stored locally or uploaded to Amazon S3 (or any S3-compatible bucket), with configurable retention and automatic scheduling.
Destructive operation: Restore overwrites the current database and all config files. Do not restore onto a running server — stop the server first, restore, then restart.
Page Layout
The page has two sections on a single /admin/backup route:
- Backup list + actions — table of all backups with per-row controls
- Scheduled backups — interval and retention settings form
UI Actions
| Action | Trigger | Behavior |
|---|---|---|
| Create backup | "Create backup" button (top-right of list card) | Opens inline form with optional label input; POSTs to /api/v1/admin/backup/create; shows spinner on button; displays success or error toast; refreshes list on completion |
| Restore | "Restore" button per row (danger) | Opens confirmation modal: "This will overwrite your current data. Continue?" + Cancel/Restore buttons; POSTs to /api/v1/admin/backup/{id}/restore; shows success/error toast |
| Delete | "Delete" button per row (danger) | Opens confirmation dialog: "Are you sure you want to delete this backup? This cannot be undone." + Cancel/Delete buttons; DELETE to /api/v1/admin/backup/{id}; refreshes list on completion |
| Upload to S3 | "Upload to S3" button per row (if is_s3 is false) | POSTs to /api/v1/admin/backup/{id}/upload-s3; shows success toast on completion; if S3 is not configured, backend returns 500 and an error toast is shown |
| Save schedule | "Save schedule" button (schedule card) | PUTs to /api/v1/admin/backup/schedule with { auto_backup_interval_days?, retention_count? }; shows success/error toast; refreshes schedule display |
Backup Shape
Each backup object returned by the API has the following shape:
interface Backup {
id: string;
label: string;
file_path: string;
size_bytes: number;
checksum_sha256: string;
is_s3: boolean;
created_at: string; // ISO 8601
expires_at: string | null;
}Schedule Shape
interface BackupSchedule {
auto_backup_interval_days: number; // 0 = disabled
retention_count: number; // minimum 1
next_scheduled_backup: number | null; // Unix timestamp
next_scheduled_backup_iso: string | null; // ISO 8601
}API Contract
All endpoints are gated by AdminMiddleware (require admin authentication) and return JSON envelopes.
1. List backups
GET /api/v1/admin/backup/listReturns all backup records.
Response 200:
{
"success": true,
"data": [ /* Backup objects */ ],
"count": 3
}2. Create backup
POST /api/v1/admin/backup/createRequest body (all fields optional):
{ "label": "before-upgrade" }Response 200:
{
"success": true,
"message": "Backup created successfully",
"data": {
"backup_id": "a1b2c3d4-...",
"file_path": "/var/backups/phlix/before-upgrade_backup_2024-01-15_10-30-00.tar.gz",
"size_bytes": 524288000
}
}3. Delete backup
DELETE /api/v1/admin/backup/{id}Response 200:
{ "success": true, "message": "Backup deleted successfully" }Response 404:
{ "success": false, "error": "Backup not found" }4. Restore backup
POST /api/v1/admin/backup/{id}/restoreDownloads from S3 if needed, verifies the SHA-256 checksum, extracts the archive, imports database.sql via mysql, then copies the archived config files over config/.
The restore is NOT atomic and there is no rollback
There is no transaction, no pre-restore snapshot and no compensating action. The checksum and extraction steps run before anything is written, so a corrupt archive is rejected harmlessly — but once the mysql import has run it cannot be undone, and a later failure while restoring config files leaves the database fully replaced and config/ partially overwritten.
Partial config state is the normal path, not just the crash path: the restore copies file-by-file and skips past entries whose parent directory it cannot create or which fail the containment check, so it can report success having written only a subset. Take your own database and config/ snapshot before restoring.
Response 200: — the backup id is interpolated into the message.
{ "success": true, "message": "Backup 'abc123' restored successfully" }Response 500: — note message carries the failure category and error the detail, not the other way round. On a checksum mismatch:
{ "success": false, "message": "Checksum mismatch", "error": "Expected <a>, got <b>" }"Restore failed" appears as the error value only when an unexpected exception escapes.
Warning: Restore is destructive. Stop the server before restoring.
5. Upload backup to S3
POST /api/v1/admin/backup/{id}/upload-s3Uploads the local backup archive to S3. After a successful upload the is_s3 flag on the backup record updates to true and file_path is replaced with the s3:// URL — the local .tar.gz is left on disk and its path is no longer recorded anywhere. See Retention for why that matters.
Response 200:
{ "success": true, "message": "Backup uploaded to S3 successfully" }Response 500:
{ "success": false, "error": "S3 upload failed" }Returns 500 if S3 is not configured in config/backup.php.
6. Get schedule settings
GET /api/v1/admin/backup/scheduleResponse 200:
{
"success": true,
"data": {
"auto_backup_interval_days": 7,
"retention_count": 10,
"next_scheduled_backup": 1705930200,
"next_scheduled_backup_iso": "2024-01-22T10:30:00Z"
}
}next_scheduled_backup and next_scheduled_backup_iso are null when auto_backup_interval_days is 0 (scheduled backups disabled).
7. Update schedule settings
PUT /api/v1/admin/backup/scheduleRequest body (all fields optional):
{ "auto_backup_interval_days": 7, "retention_count": 10 }auto_backup_interval_days: number of days between automatic backups; 0 disables auto-backup. retention_count: number of backups to retain (must be ≥ 1).
Response 200:
{
"success": true,
"message": "Schedule updated successfully",
"data": {
"auto_backup_interval_days": 7,
"retention_count": 5
}
}Response 400: — the specific rule is in message; error is the category.
{ "success": false, "error": "Invalid retention count", "message": "retention_count must be at least 1" }Configuration
Backup settings are defined in config/backup.php:
<?php
return [
'enabled' => true, // REQUIRED for automatic backups
'local_path' => '/var/phlix/backups',
'auto_backup_interval_days' => 7, // 0 = disabled
'retention_count' => 5, // keep last N backups
's3' => [
'enabled' => false,
'region' => 'us-east-1',
'bucket' => 'my-phlix-backups',
'prefix' => 'backups/',
'access_key' => null,
'secret_key' => null,
'endpoint' => null, // for S3-compatible stores (MinIO, R2, B2, etc.)
],
];enabled is required, and the schedule is only read at boot
Automatic backups need an explicit truthy enabled. The boot-time check reads it as empty($backupConfig['enabled']), so a config/backup.php that omits the key registers no timer at all, whatever auto_backup_interval_days says. (A second reader defaults the same key to true, but nothing consumes that value — the boot-time check is the only live consumer, so the behaviour is unambiguous: no enabled, no automatic backups.) Manual backups from the admin page are unaffected.
Both gates — enabled and auto_backup_interval_days > 0 — are evaluated once, at startup. PUT /api/v1/admin/backup/schedule rewrites config/backup.php but does not touch the running process, and the backup manager caches its config for the process lifetime. Restart the server for a schedule change to take effect.
When enabled is truthy and auto_backup_interval_days > 0, the server registers a scheduled-backup timer at startup. The next scheduled time is returned by GET /api/v1/admin/backup/schedule.
Retention
Old backups beyond retention_count are deleted after each new backup. Deletion removes either the S3 object or the local file, depending on the record's is_s3 flag — never both.
An uploaded backup leaves its local archive behind forever
"Upload to S3" flips is_s3 to true and replaces the stored file_path with the s3:// URL without deleting the local archive. Retention then only ever deletes the S3 object for that record, and the on-disk .tar.gz path is no longer recorded anywhere, so no code path can ever clean it up. If you use S3, prune local_path yourself.
Backup Contents
BackupManager creates tar.gz archives containing:
- MySQL database dump — all tables via
mysqldump --single-transaction - Config files — every
*.phpunderconfig/, walked recursively, so nested files such asconfig/scrobblers/trakt.phpare included (a flat glob used to miss them, losing Trakt's OAuth tokens) - User data — everything in
data/ - SSL certificates — if present at
/etc/ssl/certs/phlix,/etc/phlix/ssl, or/var/lib/phlix/ssl
See Also
- Dashboard — verify backup success in the activity feed
- Stats — storage usage over time
- Webhooks — receive alerts on backup failures
- Admin SPA dev docs — technical implementation details. There is no dedicated backup-page section:
dev/admin-spa.mddocuments sections 1–8, 14 and 16–18, and the backup page is not among them.