fix(api): enable consistent container naming for a custom internal name

Since 50268139b the API returned 422 for custom_internal_name without
is_consistent_container_name_enabled. v4.3.23 accepted it, so clients
such as `coolify app update --custom-internal-name` broke.

- A custom_internal_name sent without the flag turns the flag on, the
  same rule as the migration for existing applications
- Keep the 422 only when the request sends the flag as false
This commit is contained in:
Andras Bacsai
2026-10-05 22:37:07 +02:00
parent 41dbcbbab3
commit 1683f77f2e
4 changed files with 63 additions and 31 deletions
@@ -161,6 +161,11 @@ class ApplicationsController extends Controller
$settings['custom_container_name_prefix'] = str($settings['custom_container_name_prefix'])->slug()->value() ?: null;
}
// Container names use custom_internal_name only with consistent container naming, so a name sent alone turns it on.
if (filled($settings['custom_internal_name'] ?? null) && ! array_key_exists('is_consistent_container_name_enabled', $settings)) {
$settings['is_consistent_container_name_enabled'] = true;
}
return $settings;
}
@@ -184,16 +189,11 @@ class ApplicationsController extends Controller
}
/**
* Container names honor custom_internal_name only with consistent container naming, so reject a name that would be ignored.
* Container names honor custom_internal_name only with consistent container naming, so reject a name sent while the request turns it off.
*/
private function customInternalNameValidationResponse(array $settings, ?Application $application = null): ?JsonResponse
private function customInternalNameValidationResponse(array $settings): ?JsonResponse
{
if (! filled($settings['custom_internal_name'] ?? null)) {
return null;
}
$consistentNaming = $settings['is_consistent_container_name_enabled']
?? (bool) $application?->settings?->is_consistent_container_name_enabled;
if ($consistentNaming) {
if (! filled($settings['custom_internal_name'] ?? null) || ($settings['is_consistent_container_name_enabled'] ?? true)) {
return null;
}
@@ -439,7 +439,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
@@ -634,7 +634,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
@@ -829,7 +829,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
@@ -995,7 +995,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
@@ -1157,7 +1157,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
@@ -3151,7 +3151,7 @@ class ApplicationsController extends Controller
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'],
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
@@ -3335,7 +3335,7 @@ class ApplicationsController extends Controller
if ($prefixValidation = $this->containerNamePrefixValidationResponse($applicationSettings, $application->destination->server, $application)) {
return $prefixValidation;
}
if ($internalNameValidation = $this->customInternalNameValidationResponse($applicationSettings, $application)) {
if ($internalNameValidation = $this->customInternalNameValidationResponse($applicationSettings)) {
return $internalNameValidation;
}
$requestedBuildPack = $request->input('build_pack', $application->build_pack);
+6 -6
View File
@@ -950,7 +950,7 @@
"custom_internal_name": {
"type": "string",
"nullable": true,
"description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true."
"description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422."
},
"custom_container_name_prefix": {
"type": "string",
@@ -1541,7 +1541,7 @@
"custom_internal_name": {
"type": "string",
"nullable": true,
"description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true."
"description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422."
},
"custom_container_name_prefix": {
"type": "string",
@@ -2132,7 +2132,7 @@
"custom_internal_name": {
"type": "string",
"nullable": true,
"description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true."
"description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422."
},
"custom_container_name_prefix": {
"type": "string",
@@ -2618,7 +2618,7 @@
"custom_internal_name": {
"type": "string",
"nullable": true,
"description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true."
"description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422."
},
"custom_container_name_prefix": {
"type": "string",
@@ -3084,7 +3084,7 @@
"custom_internal_name": {
"type": "string",
"nullable": true,
"description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true."
"description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422."
},
"custom_container_name_prefix": {
"type": "string",
@@ -3795,7 +3795,7 @@
"custom_internal_name": {
"type": "string",
"nullable": true,
"description": "Custom internal container name. Requires is_consistent_container_name_enabled to be true."
"description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422."
},
"custom_container_name_prefix": {
"type": "string",
+6 -6
View File
@@ -623,7 +623,7 @@ paths:
custom_internal_name:
type: string
nullable: true
description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'
description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'
custom_container_name_prefix:
type: string
nullable: true
@@ -1009,7 +1009,7 @@ paths:
custom_internal_name:
type: string
nullable: true
description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'
description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'
custom_container_name_prefix:
type: string
nullable: true
@@ -1395,7 +1395,7 @@ paths:
custom_internal_name:
type: string
nullable: true
description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'
description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'
custom_container_name_prefix:
type: string
nullable: true
@@ -1722,7 +1722,7 @@ paths:
custom_internal_name:
type: string
nullable: true
description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'
description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'
custom_container_name_prefix:
type: string
nullable: true
@@ -2035,7 +2035,7 @@ paths:
custom_internal_name:
type: string
nullable: true
description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'
description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'
custom_container_name_prefix:
type: string
nullable: true
@@ -2502,7 +2502,7 @@ paths:
custom_internal_name:
type: string
nullable: true
description: 'Custom internal container name. Requires is_consistent_container_name_enabled to be true.'
description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'
custom_container_name_prefix:
type: string
nullable: true
@@ -51,13 +51,28 @@ function customInternalNameCreatePayload(array $overrides): array
const CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING = 'Set is_consistent_container_name_enabled to true to use custom_internal_name. Coolify ignores the custom internal name while consistent container naming is turned off.';
test('updating only custom_internal_name is rejected while consistent container naming is off', function () {
test('updating only custom_internal_name turns consistent container naming on', function () {
$this->withToken($this->bearerToken)
->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_internal_name' => 'my-app'])
->assertOk();
$settings = $this->application->fresh()->settings;
expect($settings->custom_internal_name)->toBe('my-app')
->and($settings->is_consistent_container_name_enabled)->toBeTrue();
});
test('updating custom_internal_name with consistent container naming explicitly off is rejected', function () {
$this->withToken($this->bearerToken)
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'custom_internal_name' => 'my-app',
'is_consistent_container_name_enabled' => false,
])
->assertUnprocessable()
->assertJsonPath('errors.custom_internal_name.0', CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING);
expect($this->application->fresh()->settings->custom_internal_name)->toBeNull();
$settings = $this->application->fresh()->settings;
expect($settings->custom_internal_name)->toBeNull()
->and($settings->is_consistent_container_name_enabled)->toBeFalse();
});
test('updating custom_internal_name while turning consistent container naming off is rejected', function () {
@@ -99,13 +114,30 @@ test('clearing custom_internal_name does not need consistent container naming',
$this->withToken($this->bearerToken)
->patchJson("/api/v1/applications/{$this->application->uuid}", ['custom_internal_name' => $value])
->assertOk();
expect($this->application->fresh()->settings->is_consistent_container_name_enabled)->toBeFalse();
})->with(['null' => [null], 'empty string' => ['']]);
test('creating an application with custom_internal_name requires consistent container naming', function () {
test('creating an application with only custom_internal_name turns consistent container naming on', function () {
Queue::fake();
$response = $this->withToken($this->bearerToken)
->postJson('/api/v1/applications/public', customInternalNameCreatePayload(['custom_internal_name' => 'my-app']))
->assertCreated();
$settings = Application::query()->where('uuid', $response->json('uuid'))->sole()->settings;
expect($settings->custom_internal_name)->toBe('my-app')
->and($settings->is_consistent_container_name_enabled)->toBeTrue();
});
test('creating an application with custom_internal_name and consistent container naming explicitly off is rejected', function () {
Queue::fake();
$this->withToken($this->bearerToken)
->postJson('/api/v1/applications/public', customInternalNameCreatePayload(['custom_internal_name' => 'my-app']))
->postJson('/api/v1/applications/public', customInternalNameCreatePayload([
'custom_internal_name' => 'my-app',
'is_consistent_container_name_enabled' => false,
]))
->assertUnprocessable()
->assertJsonPath('errors.custom_internal_name.0', CUSTOM_INTERNAL_NAME_NEEDS_CONSISTENT_NAMING);