Files
coolify/app/Actions/Service/DeleteService.php
T
Andras Bacsai 2b5bffe37d fix(service): delete resources from Coolify when server is unreachable
DeleteResourceJob no longer restores a service and rethrows when remote
cleanup fails. The resource is always removed from Coolify, and the team
gets a notification that Docker resources may remain on the server. No
notification is sent when the server itself was already deleted.

removeSubresourceContainer now returns false instead of throwing when the
server is not functional. Deleting a service application or database then
shows a warning that the container is removed on the next service start.

StopService handles a missing destination server, environment, project,
or team without throwing. The danger zone warning now shows for any
resource whose server is unreachable, not only services.
2026-09-30 08:02:02 +02:00

96 lines
3.6 KiB
PHP

<?php
namespace App\Actions\Service;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use RuntimeException;
class DeleteService
{
public function cleanupRemote(Service $service, bool $deleteVolumes, bool $deleteConnectedNetworks, bool $deleteConfigurations): void
{
$server = data_get($service, 'server');
if (! $server?->isFunctional()) {
throw new RuntimeException('Server is not functional.');
}
$this->removeContainers($service);
if ($deleteVolumes) {
$commands = [];
foreach ($service->applications()->get() as $application) {
foreach ($application->persistentStorages()->get() as $storage) {
$commands[] = 'docker volume rm -f '.escapeshellarg($storage->name);
}
}
foreach ($service->databases()->get() as $database) {
foreach ($database->persistentStorages()->get() as $storage) {
$commands[] = 'docker volume rm -f '.escapeshellarg($storage->name);
}
}
foreach ($commands as $command) {
instant_remote_process([$command], $server);
}
}
if ($deleteConnectedNetworks) {
$service->deleteConnectedNetworks();
}
if ($deleteConfigurations) {
$service->deleteConfigurations();
}
}
/**
* Removes the container of one service part. Returns false when the server does not respond:
* the container then stays until the service starts again (compose up --remove-orphans).
*/
public function removeSubresourceContainer(ServiceApplication|ServiceDatabase $resource): bool
{
$service = $resource->service;
if (! $service?->server?->isFunctional()) {
return false;
}
$this->removeContainers($service, $resource);
return true;
}
private function removeContainers(Service $service, ServiceApplication|ServiceDatabase|null $subresource = null): void
{
$filters = [];
$legacyFilters = [];
if ($subresource !== null) {
$subType = $subresource instanceof ServiceDatabase ? 'database' : 'application';
$filters = ["label=coolify.service.subUuid={$subresource->uuid}", "label=coolify.service.subType={$subType}"];
// Containers from before the UUID labels: the compose service key is the subresource name.
$legacyFilters = ["label=com.docker.compose.service={$subresource->name}", "label=coolify.service.subType={$subType}"];
}
// One sh -c line, so non-root servers run the whole script with sudo. A leading variable
// assignment would become "sudo container_ids=...", which sudo rejects.
$script = containerIdsByOwnerScript('service', $service->uuid, $filters, legacyExtraFilters: $legacyFilters)
.'; [ -z "$container_ids" ] || docker rm -f $container_ids';
instant_remote_process(['sh -c '.escapeshellarg($script)], $service->server);
}
public function deleteLocal(Service $service): void
{
foreach ($service->applications()->get() as $application) {
$application->forceDelete();
}
foreach ($service->databases()->get() as $database) {
$database->forceDelete();
}
foreach ($service->scheduled_tasks as $task) {
$task->delete();
}
$service->environment_variables()->delete();
$service->tags()->detach();
$service->forceDelete();
}
}