From 9253586f8fa545040388a3394c8c895b8b9da443 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:41:20 +0100 Subject: [PATCH 1/4] refactor(api): update application create endpoints docs --- app/Http/Controllers/Api/ApplicationsController.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index ca782651a..b1ca08051 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -579,8 +579,8 @@ public function create_private_deploy_key_application(Request $request) } #[OA\Post( - summary: 'Create (Dockerfile)', - description: 'Create new application based on a simple Dockerfile.', + summary: 'Create (Dockerfile without git)', + description: 'Create new application based on a simple Dockerfile (without git).', path: '/applications/dockerfile', operationId: 'create-dockerfile-application', security: [ @@ -715,8 +715,8 @@ public function create_dockerfile_application(Request $request) } #[OA\Post( - summary: 'Create (Docker Image)', - description: 'Create new application based on a prebuilt docker image', + summary: 'Create (Docker Image without git)', + description: 'Create new application based on a prebuilt docker image (without git).', path: '/applications/dockerimage', operationId: 'create-dockerimage-application', security: [ @@ -2180,7 +2180,7 @@ public function update_by_uuid(Request $request) $this->authorize('update', $application); $server = $application->destination->server; - $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings','custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled']; + $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings','custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled']; $validationRules = [ 'name' => 'string|max:255', From 7c0cb2f59d0e71df3a4f0c39c94b3db97f233f45 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:44:52 +0100 Subject: [PATCH 2/4] fix(api): deprecate applications compose endpoint - this endpoint is a duplicate of the services endpoint and also updates fields that do not exist which makes it unstable --- app/Http/Controllers/Api/ApplicationsController.php | 8 ++++++-- routes/api.php | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index b1ca08051..a08143022 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -847,11 +847,15 @@ public function create_dockerimage_application(Request $request) return $this->create_application($request, 'dockerimage'); } + /** + * @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is an unstable duplicate of POST /api/v1/services. + */ #[OA\Post( - summary: 'Create (Docker Compose)', - description: 'Create new application based on a docker-compose file.', + summary: 'Create (Docker Compose) (Deprecated)', + description: 'Create new application based on a docker-compose file (without git).', path: '/applications/dockercompose', operationId: 'create-dockercompose-application', + deprecated: true, security: [ ['bearerAuth' => []], ], diff --git a/routes/api.php b/routes/api.php index aaf7d794b..8ff1fd1cc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -103,6 +103,10 @@ Route::post('/applications/private-deploy-key', [ApplicationsController::class, 'create_private_deploy_key_application'])->middleware(['api.ability:write']); Route::post('/applications/dockerfile', [ApplicationsController::class, 'create_dockerfile_application'])->middleware(['api.ability:write']); Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware(['api.ability:write']); + + /** + * @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is a unstable duplicate of POST /api/v1/services. + */ Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware(['api.ability:write']); Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['api.ability:read']); From ce3cae3ff9bf3f1580427df21c7883b7f19e4e0f Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:42:20 +0100 Subject: [PATCH 3/4] fix(api): applications post and patch endpoints - remove `docker_compose_raw` from post and patch endpoints, as the compose file is sourced from git and should not be manually settable via the api - improve the documentation for `docker_compose_domains` (URLs) - enhanced array validation for `docker_compose_domains` by validating each array field and verifying which fields are allowed - set a custom array validation error message, as the default message is not really clear - show an error if the user attempts to set `domains` when the build pack is `dockercompose` - validate that the `domains` in `docker_compose_domains` are proper URLs and include a valid scheme (`http` or `https`) --- .../Api/ApplicationsController.php | 364 ++++++++++++------ bootstrap/helpers/api.php | 2 +- 2 files changed, 254 insertions(+), 112 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index a08143022..099b3d124 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -19,6 +19,7 @@ use App\Rules\ValidGitRepositoryUrl; use App\Services\DockerImageParser; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use OpenApi\Attributes as OA; use Spatie\Url\Url; @@ -198,10 +199,19 @@ public function applications(Request $request) 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], - 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], - 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'docker_compose_domains' => [ + 'type' => 'array', + 'description' => 'Array of URLs to be applied to containers of a dockercompose application.', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'], + 'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'], + ], + ), + ], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], @@ -350,10 +360,19 @@ public function create_public_application(Request $request) 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], - 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], - 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'docker_compose_domains' => [ + 'type' => 'array', + 'description' => 'Array of URLs to be applied to containers of a dockercompose application.', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'], + 'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'], + ], + ), + ], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], @@ -502,10 +521,19 @@ public function create_private_gh_app_application(Request $request) 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], - 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], - 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'docker_compose_domains' => [ + 'type' => 'array', + 'description' => 'Array of URLs to be applied to containers of a dockercompose application.', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'], + 'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'], + ], + ), + ], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'], @@ -1056,8 +1084,10 @@ private function create_application(Request $request, $type) 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', 'docker_compose_location' => 'string', - 'docker_compose_raw' => 'string|nullable', 'docker_compose_domains' => 'array|nullable', + 'docker_compose_domains.*' => 'array:name,domain', + 'docker_compose_domains.*.name' => 'string|required', + 'docker_compose_domains.*.domain' => 'string|nullable', ]; // ports_exposes is not required for dockercompose if ($request->build_pack === 'dockercompose') { @@ -1065,13 +1095,26 @@ private function create_application(Request $request, $type) $request->offsetSet('ports_exposes', '80'); } $validationRules = array_merge(sharedDataApplications(), $validationRules); - $validator = customApiValidator($request->all(), $validationRules); + $validationMessages = [ + 'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.', + ]; + $validator = Validator::make($request->all(), $validationRules, $validationMessages); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } + // For dockercompose applications, domains (fqdn) field should not be used + // Only docker_compose_domains should be used to set domains for individual services + if ($request->build_pack === 'dockercompose' && $request->has('domains')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.', + ], + ], 422); + } if (! $request->has('name')) { $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); } @@ -1087,11 +1130,42 @@ private function create_application(Request $request, $type) $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { $dockerComposeDomains = collect($request->docker_compose_domains); - if ($dockerComposeDomains->count() > 0) { - $dockerComposeDomains->each(function ($domain, $key) use ($dockerComposeDomainsJson) { - $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); - }); + $domainErrors = []; + + foreach ($dockerComposeDomains as $index => $item) { + $domainValue = data_get($item, 'domain'); + if (filled($domainValue)) { + $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); + str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { + $url = trim($url); + if (empty($url)) { + return; + } + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $domainErrors[] = "Invalid URL: {$url}"; + + return; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + }); + } } + + if (! empty($domainErrors)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_domains' => $domainErrors, + ], + ], 422); + } + + $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) { + $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); + }); $request->offsetUnset('docker_compose_domains'); } if ($dockerComposeDomainsJson->count() > 0) { @@ -1170,17 +1244,32 @@ private function create_application(Request $request, $type) 'github_app_uuid' => 'string|required', 'watch_paths' => 'string|nullable', 'docker_compose_location' => 'string', - 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_domains.*' => 'array:name,domain', + 'docker_compose_domains.*.name' => 'string|required', + 'docker_compose_domains.*.domain' => 'string|nullable', ]; $validationRules = array_merge(sharedDataApplications(), $validationRules); - - $validator = customApiValidator($request->all(), $validationRules); + $validationMessages = [ + 'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.', + ]; + $validator = Validator::make($request->all(), $validationRules, $validationMessages); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', 'errors' => $validator->errors(), ], 422); } + // For dockercompose applications, domains (fqdn) field should not be used + // Only docker_compose_domains should be used to set domains for individual services + if ($request->build_pack === 'dockercompose' && $request->has('domains')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.', + ], + ], 422); + } if (! $request->has('name')) { $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); @@ -1229,43 +1318,43 @@ private function create_application(Request $request, $type) $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { - if (! $request->has('docker_compose_raw')) { + $dockerComposeDomains = collect($request->docker_compose_domains); + $domainErrors = []; + + foreach ($dockerComposeDomains as $index => $item) { + $domainValue = data_get($item, 'domain'); + if (filled($domainValue)) { + $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); + str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { + $url = trim($url); + if (empty($url)) { + return; + } + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $domainErrors[] = "Invalid URL: {$url}"; + + return; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + }); + } + } + + if (! empty($domainErrors)) { return response()->json([ 'message' => 'Validation failed.', 'errors' => [ - 'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.', + 'docker_compose_domains' => $domainErrors, ], ], 422); } - if (! isBase64Encoded($request->docker_compose_raw)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $dockerComposeRaw = base64_decode($request->docker_compose_raw); - if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $yaml = Yaml::parse($dockerComposeRaw); - $services = data_get($yaml, 'services'); - $dockerComposeDomains = collect($request->docker_compose_domains); - if ($dockerComposeDomains->count() > 0) { - $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { - $name = data_get($domain, 'name'); - if (data_get($services, $name)) { - $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); - } - }); - } + $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) { + $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); + }); $request->offsetUnset('docker_compose_domains'); } if ($dockerComposeDomainsJson->count() > 0) { @@ -1335,11 +1424,17 @@ private function create_application(Request $request, $type) 'private_key_uuid' => 'string|required', 'watch_paths' => 'string|nullable', 'docker_compose_location' => 'string', - 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_domains.*' => 'array:name,domain', + 'docker_compose_domains.*.name' => 'string|required', + 'docker_compose_domains.*.domain' => 'string|nullable', ]; $validationRules = array_merge(sharedDataApplications(), $validationRules); - $validator = customApiValidator($request->all(), $validationRules); + $validationMessages = [ + 'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.', + ]; + $validator = Validator::make($request->all(), $validationRules, $validationMessages); if ($validator->fails()) { return response()->json([ @@ -1347,6 +1442,16 @@ private function create_application(Request $request, $type) 'errors' => $validator->errors(), ], 422); } + // For dockercompose applications, domains (fqdn) field should not be used + // Only docker_compose_domains should be used to set domains for individual services + if ($request->build_pack === 'dockercompose' && $request->has('domains')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.', + ], + ], 422); + } if (! $request->has('name')) { $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); } @@ -1370,44 +1475,43 @@ private function create_application(Request $request, $type) $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { - if (! $request->has('docker_compose_raw')) { + $dockerComposeDomains = collect($request->docker_compose_domains); + $domainErrors = []; + + foreach ($dockerComposeDomains as $index => $item) { + $domainValue = data_get($item, 'domain'); + if (filled($domainValue)) { + $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); + str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { + $url = trim($url); + if (empty($url)) { + return; + } + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $domainErrors[] = "Invalid URL: {$url}"; + + return; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + }); + } + } + + if (! empty($domainErrors)) { return response()->json([ 'message' => 'Validation failed.', 'errors' => [ - 'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.', + 'docker_compose_domains' => $domainErrors, ], ], 422); } - if (! isBase64Encoded($request->docker_compose_raw)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $dockerComposeRaw = base64_decode($request->docker_compose_raw); - if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $dockerComposeRaw = base64_decode($request->docker_compose_raw); - $yaml = Yaml::parse($dockerComposeRaw); - $services = data_get($yaml, 'services'); - $dockerComposeDomains = collect($request->docker_compose_domains); - if ($dockerComposeDomains->count() > 0) { - $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { - $name = data_get($domain, 'name'); - if (data_get($services, $name)) { - $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); - } - }); - } + $dockerComposeDomains->each(function ($domain) use ($dockerComposeDomainsJson) { + $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); + }); $request->offsetUnset('docker_compose_domains'); } if ($dockerComposeDomainsJson->count() > 0) { @@ -2090,10 +2194,19 @@ public function delete_by_uuid(Request $request) 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], - 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], - 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'docker_compose_domains' => [ + 'type' => 'array', + 'description' => 'Array of URLs to be applied to containers of a dockercompose application.', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'], + 'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")'], + ], + ), + ], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'], 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], @@ -2192,8 +2305,10 @@ public function update_by_uuid(Request $request) 'static_image' => 'string', 'watch_paths' => 'string|nullable', 'docker_compose_location' => 'string', - 'docker_compose_raw' => 'string|nullable', 'docker_compose_domains' => 'array|nullable', + 'docker_compose_domains.*' => 'array:name,domain', + 'docker_compose_domains.*.name' => 'string|required', + 'docker_compose_domains.*.domain' => 'string|nullable', 'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable', 'custom_nginx_configuration' => 'string|nullable', @@ -2202,7 +2317,10 @@ public function update_by_uuid(Request $request) 'http_basic_auth_password' => 'string', ]; $validationRules = array_merge(sharedDataApplications(), $validationRules); - $validator = customApiValidator($request->all(), $validationRules); + $validationMessages = [ + 'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only a name and domain field are supported.', + ]; + $validator = Validator::make($request->all(), $validationRules, $validationMessages); // Validate ports_exposes if ($request->has('ports_exposes')) { @@ -2278,6 +2396,17 @@ public function update_by_uuid(Request $request) $application->save(); } + // For dockercompose applications, domains (fqdn) field should not be used + // Only docker_compose_domains should be used to set domains for individual services + if ($application->build_pack === 'dockercompose' && $request->has('domains')) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'The domains field cannot be used for dockercompose applications. Use docker_compose_domains instead to set domains for individual services.', + ], + ], 422); + } + $domains = $request->domains; $requestHasDomains = $request->has('domains'); if ($requestHasDomains && $server->isProxyShouldRun()) { @@ -2321,44 +2450,57 @@ public function update_by_uuid(Request $request) $dockerComposeDomainsJson = collect(); if ($request->has('docker_compose_domains')) { - if (! $request->has('docker_compose_raw')) { + if (empty($application->docker_compose_raw)) { return response()->json([ 'message' => 'Validation failed.', 'errors' => [ - 'docker_compose_raw' => 'The base64 encoded docker_compose_raw is required.', + 'docker_compose_domains' => 'Cannot set docker_compose_domains without docker_compose_raw. Reload the compose file from the git repository first.', ], ], 422); } - if (! isBase64Encoded($request->docker_compose_raw)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $dockerComposeRaw = base64_decode($request->docker_compose_raw); - if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', - ], - ], 422); - } - $dockerComposeRaw = base64_decode($request->docker_compose_raw); - $yaml = Yaml::parse($dockerComposeRaw); - $services = data_get($yaml, 'services'); $dockerComposeDomains = collect($request->docker_compose_domains); - if ($dockerComposeDomains->count() > 0) { - $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { - $name = data_get($domain, 'name'); - if (data_get($services, $name)) { - $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); - } - }); + $domainErrors = []; + + foreach ($dockerComposeDomains as $item) { + $domainValue = data_get($item, 'domain'); + if (filled($domainValue)) { + $urls = str($domainValue)->replaceStart(',', '')->replaceEnd(',', '')->trim(); + str($urls)->explode(',')->each(function ($url) use (&$domainErrors) { + $url = trim($url); + if (empty($url)) { + return; + } + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $domainErrors[] = "Invalid URL: {$url}"; + + return; + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $domainErrors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + }); + } } + + if (! empty($domainErrors)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_domains' => $domainErrors, + ], + ], 422); + } + + $yaml = Yaml::parse($application->docker_compose_raw); + $services = data_get($yaml, 'services', []); + $dockerComposeDomains->each(function ($domain) use ($services, $dockerComposeDomainsJson) { + $name = data_get($domain, 'name'); + if ($name && is_array($services) && isset($services[$name])) { + $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); + } + }); $request->offsetUnset('docker_compose_domains'); } $instantDeploy = $request->instant_deploy; diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index d9d6f40d8..55cff42d0 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -131,7 +131,6 @@ function sharedDataApplications() 'manual_webhook_secret_gitea' => 'string|nullable', 'docker_compose_location' => 'string', 'docker_compose' => 'string|nullable', - 'docker_compose_raw' => 'string|nullable', 'docker_compose_domains' => 'array|nullable', 'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable', @@ -181,4 +180,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request) $request->offsetUnset('force_domain_override'); $request->offsetUnset('autogenerate_domain'); $request->offsetUnset('is_container_label_escape_enabled'); + $request->offsetUnset('docker_compose_raw'); } From bc575f1560460ebd45c843ac1686d605fb9a898a Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:42:46 +0100 Subject: [PATCH 4/4] chore(api): regenerate openapi docs --- openapi.json | 89 +++++++++++++++++++++++++++++++++++++--------------- openapi.yaml | 37 +++++++++------------- 2 files changed, 78 insertions(+), 48 deletions(-) diff --git a/openapi.json b/openapi.json index 9c31f7628..9fdec634f 100644 --- a/openapi.json +++ b/openapi.json @@ -326,10 +326,6 @@ "type": "string", "description": "The Docker Compose location." }, - "docker_compose_raw": { - "type": "string", - "description": "The Docker Compose raw content." - }, "docker_compose_custom_start_command": { "type": "string", "description": "The Docker Compose custom start command." @@ -340,7 +336,20 @@ }, "docker_compose_domains": { "type": "array", - "description": "The Docker Compose domains." + "description": "Array of URLs to be applied to containers of a dockercompose application.", + "items": { + "properties": { + "name": { + "type": "string", + "description": "The service name as defined in docker-compose." + }, + "domain": { + "type": "string", + "description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")" + } + }, + "type": "object" + } }, "watch_paths": { "type": "string", @@ -746,10 +755,6 @@ "type": "string", "description": "The Docker Compose location." }, - "docker_compose_raw": { - "type": "string", - "description": "The Docker Compose raw content." - }, "docker_compose_custom_start_command": { "type": "string", "description": "The Docker Compose custom start command." @@ -760,7 +765,20 @@ }, "docker_compose_domains": { "type": "array", - "description": "The Docker Compose domains." + "description": "Array of URLs to be applied to containers of a dockercompose application.", + "items": { + "properties": { + "name": { + "type": "string", + "description": "The service name as defined in docker-compose." + }, + "domain": { + "type": "string", + "description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")" + } + }, + "type": "object" + } }, "watch_paths": { "type": "string", @@ -1166,10 +1184,6 @@ "type": "string", "description": "The Docker Compose location." }, - "docker_compose_raw": { - "type": "string", - "description": "The Docker Compose raw content." - }, "docker_compose_custom_start_command": { "type": "string", "description": "The Docker Compose custom start command." @@ -1180,7 +1194,20 @@ }, "docker_compose_domains": { "type": "array", - "description": "The Docker Compose domains." + "description": "Array of URLs to be applied to containers of a dockercompose application.", + "items": { + "properties": { + "name": { + "type": "string", + "description": "The service name as defined in docker-compose." + }, + "domain": { + "type": "string", + "description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")" + } + }, + "type": "object" + } }, "watch_paths": { "type": "string", @@ -1318,8 +1345,8 @@ "tags": [ "Applications" ], - "summary": "Create (Dockerfile)", - "description": "Create new application based on a simple Dockerfile.", + "summary": "Create (Dockerfile without git)", + "description": "Create new application based on a simple Dockerfile (without git).", "operationId": "create-dockerfile-application", "requestBody": { "description": "Application object that needs to be created.", @@ -1667,8 +1694,8 @@ "tags": [ "Applications" ], - "summary": "Create (Docker Image)", - "description": "Create new application based on a prebuilt docker image", + "summary": "Create (Docker Image without git)", + "description": "Create new application based on a prebuilt docker image (without git).", "operationId": "create-dockerimage-application", "requestBody": { "description": "Application object that needs to be created.", @@ -1999,8 +2026,8 @@ "tags": [ "Applications" ], - "summary": "Create (Docker Compose)", - "description": "Create new application based on a docker-compose file.", + "summary": "Create (Docker Compose) (Deprecated)", + "description": "Create new application based on a docker-compose file (without git).", "operationId": "create-dockercompose-application", "requestBody": { "description": "Application object that needs to be created.", @@ -2153,6 +2180,7 @@ } } }, + "deprecated": true, "security": [ { "bearerAuth": [] @@ -2558,10 +2586,6 @@ "type": "string", "description": "The Docker Compose location." }, - "docker_compose_raw": { - "type": "string", - "description": "The Docker Compose raw content." - }, "docker_compose_custom_start_command": { "type": "string", "description": "The Docker Compose custom start command." @@ -2572,7 +2596,20 @@ }, "docker_compose_domains": { "type": "array", - "description": "The Docker Compose domains." + "description": "Array of URLs to be applied to containers of a dockercompose application.", + "items": { + "properties": { + "name": { + "type": "string", + "description": "The service name as defined in docker-compose." + }, + "domain": { + "type": "string", + "description": "Comma-separated list of URLs (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")" + } + }, + "type": "object" + } }, "watch_paths": { "type": "string", diff --git a/openapi.yaml b/openapi.yaml index 198322328..383481a8d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -237,9 +237,6 @@ paths: docker_compose_location: type: string description: 'The Docker Compose location.' - docker_compose_raw: - type: string - description: 'The Docker Compose raw content.' docker_compose_custom_start_command: type: string description: 'The Docker Compose custom start command.' @@ -248,7 +245,8 @@ paths: description: 'The Docker Compose custom build command.' docker_compose_domains: type: array - description: 'The Docker Compose domains.' + description: 'Array of URLs to be applied to containers of a dockercompose application.' + items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object } watch_paths: type: string description: 'The watch paths.' @@ -511,9 +509,6 @@ paths: docker_compose_location: type: string description: 'The Docker Compose location.' - docker_compose_raw: - type: string - description: 'The Docker Compose raw content.' docker_compose_custom_start_command: type: string description: 'The Docker Compose custom start command.' @@ -522,7 +517,8 @@ paths: description: 'The Docker Compose custom build command.' docker_compose_domains: type: array - description: 'The Docker Compose domains.' + description: 'Array of URLs to be applied to containers of a dockercompose application.' + items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object } watch_paths: type: string description: 'The watch paths.' @@ -785,9 +781,6 @@ paths: docker_compose_location: type: string description: 'The Docker Compose location.' - docker_compose_raw: - type: string - description: 'The Docker Compose raw content.' docker_compose_custom_start_command: type: string description: 'The Docker Compose custom start command.' @@ -796,7 +789,8 @@ paths: description: 'The Docker Compose custom build command.' docker_compose_domains: type: array - description: 'The Docker Compose domains.' + description: 'Array of URLs to be applied to containers of a dockercompose application.' + items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object } watch_paths: type: string description: 'The watch paths.' @@ -860,8 +854,8 @@ paths: post: tags: - Applications - summary: 'Create (Dockerfile)' - description: 'Create new application based on a simple Dockerfile.' + summary: 'Create (Dockerfile without git)' + description: 'Create new application based on a simple Dockerfile (without git).' operationId: create-dockerfile-application requestBody: description: 'Application object that needs to be created.' @@ -1081,8 +1075,8 @@ paths: post: tags: - Applications - summary: 'Create (Docker Image)' - description: 'Create new application based on a prebuilt docker image' + summary: 'Create (Docker Image without git)' + description: 'Create new application based on a prebuilt docker image (without git).' operationId: create-dockerimage-application requestBody: description: 'Application object that needs to be created.' @@ -1293,8 +1287,8 @@ paths: post: tags: - Applications - summary: 'Create (Docker Compose)' - description: 'Create new application based on a docker-compose file.' + summary: 'Create (Docker Compose) (Deprecated)' + description: 'Create new application based on a docker-compose file (without git).' operationId: create-dockercompose-application requestBody: description: 'Application object that needs to be created.' @@ -1374,6 +1368,7 @@ paths: warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' } conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } } type: object + deprecated: true security: - bearerAuth: [] @@ -1665,9 +1660,6 @@ paths: docker_compose_location: type: string description: 'The Docker Compose location.' - docker_compose_raw: - type: string - description: 'The Docker Compose raw content.' docker_compose_custom_start_command: type: string description: 'The Docker Compose custom start command.' @@ -1676,7 +1668,8 @@ paths: description: 'The Docker Compose custom build command.' docker_compose_domains: type: array - description: 'The Docker Compose domains.' + description: 'Array of URLs to be applied to containers of a dockercompose application.' + items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "http://app.coolify.io,https://app2.coolify.io")' } }, type: object } watch_paths: type: string description: 'The watch paths.'