diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 57ea276897..da1255eb08 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -1023,7 +1023,6 @@ class ServicesController extends Controller
new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup when the server disk usage is at or above its cleanup threshold. Skipped when a cleanup ran on the server in the last hour.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
- new OA\Parameter(name: 'delete_from_coolify_only', in: 'query', required: false, description: 'Remove only Coolify metadata without deleting Docker resources.', schema: new OA\Schema(type: 'boolean', default: false)),
],
responses: [
new OA\Response(
@@ -1054,6 +1053,7 @@ class ServicesController extends Controller
ref: '#/components/responses/404',
),
new OA\Response(response: 403, description: 'The token needs the write and deploy permissions.'),
+ new OA\Response(response: 422, ref: '#/components/responses/422'),
]
)]
public function delete_by_uuid(Request $request)
@@ -1072,10 +1072,14 @@ class ServicesController extends Controller
$this->authorize('delete', $service);
- $service->delete();
+ // The parameter was removed. Reject it instead of ignoring it, so old clients do not delete Docker resources they asked to keep.
+ if ($request->boolean('delete_from_coolify_only')) {
+ return response()->json([
+ 'message' => 'The delete_from_coolify_only parameter is no longer supported. Nothing was deleted. Send the request without it: Coolify removes the service also when the server is not reachable.',
+ ], 422);
+ }
- $deleteFromCoolifyOnlyRequested = $request->boolean('delete_from_coolify_only');
- $deleteFromCoolifyOnly = $deleteFromCoolifyOnlyRequested || ! $service->server?->isFunctional();
+ $service->delete();
DeleteResourceJob::dispatch(
resource: $service,
@@ -1083,23 +1087,15 @@ class ServicesController extends Controller
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
deleteConfigurations: $request->boolean('delete_configurations', true),
dockerCleanup: $request->boolean('docker_cleanup', true),
- deleteFromCoolifyOnly: $deleteFromCoolifyOnly,
);
auditLog('api.service.deleted', [
'team_id' => $teamId,
'service_uuid' => $service->uuid,
'service_name' => $service->name,
- 'delete_from_coolify_only' => $deleteFromCoolifyOnly,
]);
- $message = match (true) {
- $deleteFromCoolifyOnlyRequested => 'The service will be removed from Coolify only; Docker resources will remain.',
- $deleteFromCoolifyOnly => 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.',
- default => 'Service deletion request queued.',
- };
-
- return response()->json(['message' => $message]);
+ return response()->json(['message' => 'Service deletion request queued.']);
}
#[OA\Patch(
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index 0865974828..d79532e6e8 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -132,32 +132,6 @@ class Danger extends Component
}
}
- public function deleteFromCoolifyOnly(string $password): mixed
- {
- if (! verifyPasswordConfirmation($password, $this)) {
- return 'The provided password is incorrect.';
- }
-
- if (! $this->resource instanceof Service) {
- return 'Service not found.';
- }
-
- try {
- $this->authorize('delete', $this->resource);
- DeleteResourceJob::dispatch(
- resource: $this->resource,
- deleteFromCoolifyOnly: true,
- )->afterResponse();
-
- return redirectRoute($this, 'project.resource.index', [
- 'project_uuid' => $this->projectUuid,
- 'environment_uuid' => $this->environmentUuid,
- ]);
- } catch (\Throwable $e) {
- return handleError($e, $this);
- }
- }
-
public function flush($password)
{
try {
diff --git a/app/Mcp/Tools/ManageService.php b/app/Mcp/Tools/ManageService.php
index efa6d99fdd..cfd5b38c75 100644
--- a/app/Mcp/Tools/ManageService.php
+++ b/app/Mcp/Tools/ManageService.php
@@ -17,7 +17,7 @@ class ManageService extends RestActionTool
{
return [
'update' => ['method' => 'PATCH', 'path' => 'services/{uuid}', 'ability' => 'write', 'description' => 'update the service (data: name, description, docker_compose_raw base64, connect_to_docker_network, urls, force_domain_override, is_container_label_escape_enabled, redact_all_env_values_in_logs, instant_deploy; instant_deploy needs deploy ability)'],
- 'delete' => ['method' => 'DELETE', 'path' => 'services/{uuid}', 'ability' => 'deploy', 'confirm' => true, 'description' => 'delete the service (data: delete_volumes, delete_connected_networks, delete_configurations, docker_cleanup, delete_from_coolify_only)'],
+ 'delete' => ['method' => 'DELETE', 'path' => 'services/{uuid}', 'ability' => 'deploy', 'confirm' => true, 'description' => 'delete the service (data: delete_volumes, delete_connected_networks, delete_configurations, docker_cleanup)'],
'clone' => ['method' => 'POST', 'path' => 'services/{uuid}/clone', 'ability' => 'write', 'confirm' => true, 'description' => 'clone the service to a destination of the team (data: destination_uuid, name, clone_volumes; clone_volumes stops the source service while volumes are copied and needs deploy ability)'],
'move' => ['method' => 'POST', 'path' => 'services/{uuid}/move', 'ability' => 'write', 'confirm' => true, 'description' => 'move the service to another environment of the team (data: environment_uuid)'],
'update_application' => ['method' => 'PATCH', 'path' => 'services/{uuid}/applications/{app_uuid}', 'ability' => 'write', 'description' => 'update a service application (data: url, noindex_domains, human_name, description, image, exclude_from_status, is_log_drain_enabled, is_gzip_enabled, traefik_certificate_resolver, is_stripprefix_enabled, is_force_https_enabled, max_restart_count)'],
diff --git a/openapi.json b/openapi.json
index 5978a99124..7bd46ddba5 100644
--- a/openapi.json
+++ b/openapi.json
@@ -20486,16 +20486,6 @@
"type": "boolean",
"default": true
}
- },
- {
- "name": "delete_from_coolify_only",
- "in": "query",
- "description": "Remove only Coolify metadata without deleting Docker resources.",
- "required": false,
- "schema": {
- "type": "boolean",
- "default": false
- }
}
],
"responses": {
@@ -20526,6 +20516,9 @@
},
"403": {
"description": "The token needs the write and deploy permissions."
+ },
+ "422": {
+ "$ref": "#\/components\/responses\/422"
}
},
"security": [
diff --git a/openapi.yaml b/openapi.yaml
index ad222762e4..5cfd3c35c8 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -12946,14 +12946,6 @@ paths:
schema:
type: boolean
default: true
- -
- name: delete_from_coolify_only
- in: query
- description: 'Remove only Coolify metadata without deleting Docker resources.'
- required: false
- schema:
- type: boolean
- default: false
responses:
'200':
description: 'Delete a service by UUID'
@@ -12971,6 +12963,8 @@ paths:
$ref: '#/components/responses/404'
'403':
description: 'The token needs the write and deploy permissions.'
+ '422':
+ $ref: '#/components/responses/422'
security:
-
bearerAuth: []
diff --git a/resources/views/livewire/project/shared/danger.blade.php b/resources/views/livewire/project/shared/danger.blade.php
index 5aaf304ef1..73eaaac738 100644
--- a/resources/views/livewire/project/shared/danger.blade.php
+++ b/resources/views/livewire/project/shared/danger.blade.php
@@ -54,19 +54,6 @@
confirmationText="{{ $resourceName }}"
confirmationLabel="Enter the resource name to confirm permanent deletion"
shortConfirmationLabel="Resource name" />
- @if ($resource instanceof \App\Models\Service)
-
- @endif
@else
Delete {{ $resourceLabel }}
diff --git a/tests/Feature/Api/LifecycleApisTest.php b/tests/Feature/Api/LifecycleApisTest.php
index f761d0d218..0776a20612 100644
--- a/tests/Feature/Api/LifecycleApisTest.php
+++ b/tests/Feature/Api/LifecycleApisTest.php
@@ -332,14 +332,14 @@ describe('DELETE resource endpoints', function () {
->assertOk();
$response->assertJson([
- 'message' => 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.',
+ 'message' => 'Service deletion request queued.',
]);
expect(Service::find($service->id))->toBeNull()
->and(Service::withTrashed()->find($service->id)?->trashed())->toBeTrue();
Queue::assertPushed(
DeleteResourceJob::class,
- fn (DeleteResourceJob $job): bool => $job->deleteFromCoolifyOnly
+ fn (DeleteResourceJob $job): bool => $job->resource->is($service) && ! $job->deleteFromCoolifyOnly
);
});
diff --git a/tests/Feature/Api/ServiceDeleteMessageApiTest.php b/tests/Feature/Api/ServiceDeleteMessageApiTest.php
index cdee77e6c6..e655156f30 100644
--- a/tests/Feature/Api/ServiceDeleteMessageApiTest.php
+++ b/tests/Feature/Api/ServiceDeleteMessageApiTest.php
@@ -50,22 +50,17 @@ function setServiceDeleteServerReachable(Server $server, bool $reachable): void
Server::flushIdentityMap();
}
-test('explicit delete from coolify only does not claim the server is unreachable', function (bool $serverReachable) {
- setServiceDeleteServerReachable($this->server, $serverReachable);
-
+test('delete from coolify only is rejected and deletes nothing', function () {
$this->withToken($this->bearerToken)
->deleteJson("/api/v1/services/{$this->service->uuid}?delete_from_coolify_only=true")
- ->assertOk()
- ->assertJsonPath('message', 'The service will be removed from Coolify only; Docker resources will remain.');
+ ->assertUnprocessable();
- Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job): bool => $job->deleteFromCoolifyOnly);
-})->with([
- 'reachable server' => [true],
- 'unreachable server' => [false],
-]);
+ expect(Service::find($this->service->id))->not->toBeNull();
+ Queue::assertNotPushed(DeleteResourceJob::class);
+});
-test('delete on a reachable server queues a full deletion', function () {
- setServiceDeleteServerReachable($this->server, true);
+test('delete queues a full deletion whether the server is reachable or not', function (bool $serverReachable) {
+ setServiceDeleteServerReachable($this->server, $serverReachable);
$this->withToken($this->bearerToken)
->deleteJson("/api/v1/services/{$this->service->uuid}")
@@ -73,15 +68,7 @@ test('delete on a reachable server queues a full deletion', function () {
->assertJsonPath('message', 'Service deletion request queued.');
Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job): bool => ! $job->deleteFromCoolifyOnly);
-});
-
-test('delete on an unreachable server falls back to removing from coolify only', function () {
- setServiceDeleteServerReachable($this->server, false);
-
- $this->withToken($this->bearerToken)
- ->deleteJson("/api/v1/services/{$this->service->uuid}")
- ->assertOk()
- ->assertJsonPath('message', 'Server is not reachable. The service will be removed from Coolify only; Docker resources may remain.');
-
- Queue::assertPushed(DeleteResourceJob::class, fn (DeleteResourceJob $job): bool => $job->deleteFromCoolifyOnly);
-});
+})->with([
+ 'reachable server' => [true],
+ 'unreachable server' => [false],
+]);
diff --git a/tests/v4/Feature/DangerDeleteResourceTest.php b/tests/v4/Feature/DangerDeleteResourceTest.php
index 25cdf671a2..64dd56ede9 100644
--- a/tests/v4/Feature/DangerDeleteResourceTest.php
+++ b/tests/v4/Feature/DangerDeleteResourceTest.php
@@ -149,29 +149,3 @@ test('delete applies selectedActions from checkbox state', function () {
expect($component->get('delete_configurations'))->toBeTrue();
expect($component->get('docker_cleanup'))->toBeTrue();
});
-
-test('service can be removed from Coolify without remote cleanup', function () {
- $service = Service::factory()->create([
- 'environment_id' => $this->environment->id,
- 'server_id' => $this->server->id,
- 'destination_id' => $this->destination->id,
- 'destination_type' => $this->destination->getMorphClass(),
- ]);
-
- Livewire::test(Danger::class, ['resource' => $service])
- ->set('projectUuid', $this->project->uuid)
- ->set('environmentUuid', $this->environment->uuid)
- ->assertSee('Server is not reachable')
- ->assertSee('Remove from Coolify only')
- ->call('deleteFromCoolifyOnly', 'test-password')
- ->assertHasNoErrors()
- ->assertRedirectToRoute('project.resource.index', [
- 'project_uuid' => $this->project->uuid,
- 'environment_uuid' => $this->environment->uuid,
- ]);
-
- Queue::assertPushed(
- DeleteResourceJob::class,
- fn (DeleteResourceJob $job): bool => $job->resource->is($service) && $job->deleteFromCoolifyOnly
- );
-});