coollabsio/coolify · error · RuntimeException

The backup target is not a directory or persistent volume.

Error message

The backup target is not a directory or persistent volume.

What it means

Thrown by ScheduledVolumeBackup::sourcePath() (app/Models/ScheduledVolumeBackup.php:171). When computing the path to back up, the routine accepts only two shapes: a LocalPersistentVolume (uses host_path or name) or a LocalFileVolume with is_directory=true (a directory backup). If the morph target backupable is anything else — a LocalFileVolume flagged as a file, an unrelated morph type, or a dangling morph — the RuntimeException fires. This usually means the storage's directory flag was flipped or the target was mutated after the schedule was created.

Source

Thrown at app/Models/ScheduledVolumeBackup.php:171

    public function targetName(): string
    {
        return match (true) {
            $this->backupable instanceof LocalFileVolume => $this->backupable->fs_path,
            $this->backupable instanceof LocalPersistentVolume => $this->backupable->name,
            default => 'Unknown storage',
        };
    }

    public function sourcePath(): string
    {
        $target = $this->backupable;

        if ($target instanceof LocalPersistentVolume) {
            return filled($target->host_path) ? $target->host_path : $target->name;
        }

        if (! $target instanceof LocalFileVolume || ! $target->is_directory) {
            throw new \RuntimeException('The backup target is not a directory or persistent volume.');
        }

        $path = str($target->fs_path);
        if ($path->startsWith('.')) {
            $resource = $this->targetResource();
            if (! $resource || ! method_exists($resource, 'workdir')) {
                throw new \RuntimeException('The directory backup workdir is unavailable.');
            }

            return $resource->workdir().$path->after('.')->toString();
        }

        return $path->toString();
    }
}

View on GitHub (pinned to 70b9acc424)

Solutions

  1. Mark the target storage as a directory again (is_directory=true in the storage settings) if a directory backup is intended.
  2. If the storage genuinely became a file mount, delete the stale ScheduledVolumeBackup — it can never run against a file target.
  3. Guard schedule creation: only offer backup schedules for volumes and directory storages (the scopeForApplication/scopeForService query shapes show the allowed combinations).

Example fix

// before
$backup = ScheduledVolumeBackup::find($uuid);
$path = $backup->sourcePath(); // throws: target is a file storage, not a directory

// after — verify the morph target shape before computing the path
$target = $backup->backupable;
if (! $target instanceof \App\Models\LocalPersistentVolume
    && ! ($target instanceof \App\Models\LocalFileVolume && $target->is_directory)) {
    $backup->delete(); // stale schedule
    return;
}
$path = $backup->sourcePath();
Defensive patterns

Strategy: type-guard

Validate before calling

// Before creating a schedule, confirm the target is a volume or a directory storage
$ok = $target instanceof \App\Models\LocalPersistentVolume
    || ($target instanceof \App\Models\LocalFileVolume && $target->is_directory && ! $target->is_host_file);
if (! $ok) {
    abort(422, 'Backups are only supported for persistent volumes and directory storages.');
}

Type guard

function isBackupableDirectoryOrVolume(object $target): bool
{
    if ($target instanceof \App\Models\LocalPersistentVolume) {
        return true;
    }

    return $target instanceof \App\Models\LocalFileVolume && $target->is_directory;
}

Try / catch

try {
    $path = $backup->sourcePath();
} catch (\RuntimeException $e) {
    if (str_contains($e->getMessage(), 'not a directory or persistent volume')) {
        // target was flipped to a file or is a dangling morph: delete the stale schedule
        $backup->delete();
        return;
    }
    throw $e;
}

Prevention

When it happens

Trigger: Running a ScheduledVolumeBackup whose backupable is a LocalFileVolume with is_directory=false (someone switched the storage from directory to file), or whose backupable_type/backupable_id pair no longer resolves to a valid volume/directory target (dangling morph after partial cleanup).

Common situations: The self-heal path in LocalFileVolume::saveStorageOnServer() flipping is_directory to false (errors 304/305) on a storage that had a backup schedule; editing service templates that change storage types; manual DB edits to local_file_volumes rows.

Related errors


AI-assisted analysis of coollabsio/coolify@70b9acc424 (2026-08-17). Data as JSON: /api/errors/18fd100444e86290. Report an issue: GitHub.