getgrav/grav · error · RuntimeException
Backup location: {$backup_root} does not exist...
Error message
Backup location: {$backup_root} does not exist... What it means
After resolving a backup profile's root — either through the stream wrapper (locator->findResource) or by concatenating GRAV_ROOT with the relative root — Backups::backup() verifies the path exists. If the resolved $backup_root is empty or file_exists() returns false, it throws RuntimeException 'Backup location ... does not exist...'. The profile's configured root points at a directory that is not on disk.
Source
Thrown at system/src/Grav/Common/Backup/Backups.php:256
throw new RuntimeException('No backups defined...');
}
$name = $grav['inflector']->underscorize($backup->name);
$date = date(static::BACKUP_DATE_FORMAT, time());
$filename = trim((string) $name, '_') . '--' . $date . '.zip';
$grav['backups']->setup();
$destination = static::$backup_dir . DS . $filename;
$max_execution_time = ini_set('max_execution_time', '600');
$backup_root = $backup->root;
if ($locator->isStream($backup_root)) {
$backup_root = $locator->findResource($backup_root);
} else {
$backup_root = rtrim(GRAV_ROOT . $backup_root, DS) ?: DS;
}
if (!$backup_root || !file_exists($backup_root)) {
throw new RuntimeException("Backup location: {$backup_root} does not exist...");
}
// Security: Resolve real path and ensure it's within GRAV_ROOT to prevent path traversal
$realBackupRoot = realpath($backup_root);
$realGravRoot = realpath(GRAV_ROOT);
if ($realBackupRoot === false || $realGravRoot === false) {
throw new RuntimeException("Invalid backup location: {$backup_root}");
}
// Positive containment (GHSA-fch7-cpv4-w7hg): the resolved backup root must
// BE GRAV_ROOT or a directory beneath it. The previous deny-list only rejected
// a fixed set of system paths, so a non-blocklisted external directory (e.g.
// /opt, /mnt, /srv) still fell through and had its contents archived. Comparing
// against GRAV_ROOT with a trailing separator also prevents a sibling directory
// (e.g. `/var/www/site-evil` next to `/var/www/site`) from matching by prefix.
$isWithinGravRoot = $realBackupRoot === $realGravRoot
|| strpos($realBackupRoot, $realGravRoot . DIRECTORY_SEPARATOR) === 0;View on GitHub (pinned to 6040efed04)
Solutions
- Check the profile's root field in backups.profiles and confirm the directory actually exists under GRAV_ROOT
- Use the default root '/' (whole site) or a stream root like 'user://' that is guaranteed to resolve
- If using a stream, verify the scheme is defined in streams.schemes and the target directory exists
- Recreate the missing directory or repoint the profile before scheduling/calling the backup
Example fix
# user/config/backups.yaml — before
profiles:
- name: 'Pages Backup'
root: '/content' # directory does not exist
# after
profiles:
- name: 'Pages Backup'
root: '/user/pages' Defensive patterns
Strategy: validation
Validate before calling
$profile = Backups::getBackupProfiles()[$id] ?? null;
$root = $profile['root'] ?? '/';
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$resolved = $locator->isStream($root) ? $locator->findResource($root) : rtrim(GRAV_ROOT . $root, DS);
if (!$resolved || !is_dir($resolved)) {
throw new RuntimeException("Backup root {$root} resolves to a missing directory");
} Try / catch
try {
Backups::backup($id);
} catch (RuntimeException $e) {
if (str_contains($e->getMessage(), 'does not exist')) {
// config points at a moved/deleted directory — fix the profile
}
throw $e;
} Prevention
- Prefer stream roots (user://, /) over absolute paths — streams resolve against current layout
- Validate profile roots after site migrations and environment changes
- Alert on scheduled-backup failure so a silently-moved directory is noticed early
When it happens
Trigger: A profile with root: '/nonexistent-dir'; a stream root like 'user://pages' when the stream resolves to false (undefined scheme or missing directory); a root that was valid when the profile was saved but whose directory has since been moved, renamed, or deleted; a leading double slash or typo producing a bogus GRAV_ROOT concatenation.
Common situations: Site migrated to a new server and custom profile roots were not created; environment-specific configuration resolving streams differently than where the profile was authored; typo in the root field of a hand-edited backups.yaml; a directory removed by cache purging or cleanup scripts.
Related errors
- No backups defined...
- Invalid backup location: {$backup_root}
- Backup location not allowed (outside site root): {$backup_ro
- Cache folder not defined.
- Setup: Configuration reload loop detected!
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/714a127963fcc112.
Report an issue: GitHub.