{"record":{"id":"18fd100444e86290","repo":"coollabsio/coolify","slug":"the-backup-target-is-not-a-directory-or-persistent","errorCode":null,"errorMessage":"The backup target is not a directory or persistent volume.","messagePattern":"The backup target is not a directory or persistent volume\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"app/Models/ScheduledVolumeBackup.php","lineNumber":171,"sourceCode":"    public function targetName(): string\n    {\n        return match (true) {\n            $this->backupable instanceof LocalFileVolume => $this->backupable->fs_path,\n            $this->backupable instanceof LocalPersistentVolume => $this->backupable->name,\n            default => 'Unknown storage',\n        };\n    }\n\n    public function sourcePath(): string\n    {\n        $target = $this->backupable;\n\n        if ($target instanceof LocalPersistentVolume) {\n            return filled($target->host_path) ? $target->host_path : $target->name;\n        }\n\n        if (! $target instanceof LocalFileVolume || ! $target->is_directory) {\n            throw new \\RuntimeException('The backup target is not a directory or persistent volume.');\n        }\n\n        $path = str($target->fs_path);\n        if ($path->startsWith('.')) {\n            $resource = $this->targetResource();\n            if (! $resource || ! method_exists($resource, 'workdir')) {\n                throw new \\RuntimeException('The directory backup workdir is unavailable.');\n            }\n\n            return $resource->workdir().$path->after('.')->toString();\n        }\n\n        return $path->toString();\n    }\n}\n","sourceCodeStart":153,"sourceCodeEnd":187,"githubUrl":"https://github.com/coollabsio/coolify/blob/70b9acc42467278373e00de77abb40684e25b395/app/Models/ScheduledVolumeBackup.php#L153-L187","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Mark the target storage as a directory again (is_directory=true in the storage settings) if a directory backup is intended.","If the storage genuinely became a file mount, delete the stale ScheduledVolumeBackup — it can never run against a file target.","Guard schedule creation: only offer backup schedules for volumes and directory storages (the scopeForApplication/scopeForService query shapes show the allowed combinations)."],"exampleFix":"// before\n$backup = ScheduledVolumeBackup::find($uuid);\n$path = $backup->sourcePath(); // throws: target is a file storage, not a directory\n\n// after — verify the morph target shape before computing the path\n$target = $backup->backupable;\nif (! $target instanceof \\App\\Models\\LocalPersistentVolume\n    && ! ($target instanceof \\App\\Models\\LocalFileVolume && $target->is_directory)) {\n    $backup->delete(); // stale schedule\n    return;\n}\n$path = $backup->sourcePath();","handlingStrategy":"type-guard","validationCode":"// Before creating a schedule, confirm the target is a volume or a directory storage\n$ok = $target instanceof \\App\\Models\\LocalPersistentVolume\n    || ($target instanceof \\App\\Models\\LocalFileVolume && $target->is_directory && ! $target->is_host_file);\nif (! $ok) {\n    abort(422, 'Backups are only supported for persistent volumes and directory storages.');\n}","typeGuard":"function isBackupableDirectoryOrVolume(object $target): bool\n{\n    if ($target instanceof \\App\\Models\\LocalPersistentVolume) {\n        return true;\n    }\n\n    return $target instanceof \\App\\Models\\LocalFileVolume && $target->is_directory;\n}","tryCatchPattern":"try {\n    $path = $backup->sourcePath();\n} catch (\\RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'not a directory or persistent volume')) {\n        // target was flipped to a file or is a dangling morph: delete the stale schedule\n        $backup->delete();\n        return;\n    }\n    throw $e;\n}","preventionTips":["Only expose backup scheduling for LocalPersistentVolume and is_directory LocalFileVolume targets (mirror scopeForApplication/scopeForService).","When changing a storage's type from directory to file, delete its backup schedules in the same action.","Remember the saveStorageOnServer() self-heal can flip is_directory=false behind your back (errors 304/305) — re-check schedules after that fires."],"tags":["coolify","backups","polymorphic-relations","file-storage","volume"],"backgroundTag":"invalid-morph-target","analyzedSha":"70b9acc42467278373e00de77abb40684e25b395","analyzedAt":"2026-08-17T01:41:01.313Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}