From 5ee8015a0fc7cea3c0602a00a71bcb027db33268 Mon Sep 17 00:00:00 2001 From: Praveen Pendyala Date: Tue, 25 Nov 2025 13:14:09 +0000 Subject: [PATCH 01/51] Add Transcript LOL link and logo to README My account is a small sponsor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a84b3bfa9..276ef07b5 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Thank you so much! Niklas Lausch Cap-go InterviewPal +Transcript LOL ...and many more at [GitHub Sponsors](https://github.com/sponsors/coollabsio) From 7c7c09aa3d0f996e91525a35ae66e9e0818e4c91 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:29:11 +0100 Subject: [PATCH 02/51] fix(api): create service validation and docs - if service type and docker_compose_raw is filled show an error - if service type is not valid show an error with all valid service types - remove enum from service type docs as it always gets outdated --- .../Controllers/Api/ServicesController.php | 107 +++--------------- 1 file changed, 13 insertions(+), 94 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 6a00818aa..ec3d036bc 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -105,98 +105,7 @@ class ServicesController extends Controller type: 'object', required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'], properties: [ - 'type' => [ - 'description' => 'The one-click service type', - 'type' => 'string', - 'enum' => [ - 'activepieces', - 'appsmith', - 'appwrite', - 'authentik', - 'babybuddy', - 'budge', - 'changedetection', - 'chatwoot', - 'classicpress-with-mariadb', - 'classicpress-with-mysql', - 'classicpress-without-database', - 'cloudflared', - 'code-server', - 'dashboard', - 'directus', - 'directus-with-postgresql', - 'docker-registry', - 'docuseal', - 'docuseal-with-postgres', - 'dokuwiki', - 'duplicati', - 'emby', - 'embystat', - 'fider', - 'filebrowser', - 'firefly', - 'formbricks', - 'ghost', - 'gitea', - 'gitea-with-mariadb', - 'gitea-with-mysql', - 'gitea-with-postgresql', - 'glance', - 'glances', - 'glitchtip', - 'grafana', - 'grafana-with-postgresql', - 'grocy', - 'heimdall', - 'homepage', - 'jellyfin', - 'kuzzle', - 'listmonk', - 'logto', - 'mediawiki', - 'meilisearch', - 'metabase', - 'metube', - 'minio', - 'moodle', - 'n8n', - 'n8n-with-postgresql', - 'next-image-transformation', - 'nextcloud', - 'nocodb', - 'odoo', - 'openblocks', - 'pairdrop', - 'penpot', - 'phpmyadmin', - 'pocketbase', - 'posthog', - 'reactive-resume', - 'rocketchat', - 'shlink', - 'slash', - 'snapdrop', - 'statusnook', - 'stirling-pdf', - 'supabase', - 'syncthing', - 'tolgee', - 'trigger', - 'trigger-with-external-database', - 'twenty', - 'umami', - 'unleash-with-postgresql', - 'unleash-without-database', - 'uptime-kuma', - 'vaultwarden', - 'vikunja', - 'weblate', - 'whoogle', - 'wordpress-with-mariadb', - 'wordpress-with-mysql', - 'wordpress-without-database', - ], - ], + 'type' => ['description' => 'The one-click service type (e.g. "actualbudget", "calibre-web", "gitea-with-mysql" ...)', 'type' => 'string'], 'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'], 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'], 'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'], @@ -283,6 +192,13 @@ class ServicesController extends Controller 'errors' => $errors, ], 422); } + + if (filled($request->type) && filled($request->docker_compose_raw)) { + return response()->json([ + 'message' => 'You cannot provide both service type and docker_compose_raw. Use one or the other.', + ], 422); + } + $environmentUuid = $request->environment_uuid; $environmentName = $request->environment_name; if (blank($environmentUuid) && blank($environmentName)) { @@ -524,8 +440,11 @@ class ServicesController extends Controller 'uuid' => $service->uuid, 'domains' => $domains, ])->setStatusCode(201); - } else { - return response()->json(['message' => 'No service type or docker_compose_raw provided.'], 400); + } elseif (filled($request->type)) { + return response()->json([ + 'message' => 'Invalid service type.', + 'valid_service_types' => $serviceKeys, + ], 404); } } From 27a4cb47ef3b9243bb3af4de1dc3d93d7abe45ec Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:51:10 +0100 Subject: [PATCH 03/51] fix(service): signoz metrics env (#7927) --- templates/compose/signoz.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/signoz.yaml b/templates/compose/signoz.yaml index 42e21790c..c7e044947 100644 --- a/templates/compose/signoz.yaml +++ b/templates/compose/signoz.yaml @@ -450,7 +450,7 @@ services: - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__AUTH__USERNAME} - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__FROM} - SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST=${SIGNOZ_ALERTMANAGER_SIGNOZ_GLOBAL_SMTP__SMARTHOST} - - DOT_METRICS_ENABELD=true + - DOT_METRICS_ENABLED=true healthcheck: test: - CMD From 7556ce2f66cfc7b92dab18af8a7371eb30ea45b8 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik <37010174+vitabaks@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:56:05 +0500 Subject: [PATCH 04/51] feat(service): update autobase to version 2.5 (#7923) --- templates/compose/autobase.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/compose/autobase.yaml b/templates/compose/autobase.yaml index 5ee398f3e..caafa8cce 100644 --- a/templates/compose/autobase.yaml +++ b/templates/compose/autobase.yaml @@ -6,7 +6,7 @@ services: autobase: - image: autobase/console_ui:2.4.1 + image: autobase/console_ui:2.5.2 platform: linux/amd64 environment: - SERVICE_FQDN_AUTOBASE_80 @@ -22,7 +22,7 @@ services: condition: service_healthy autobase-db: - image: autobase/console_db:2.4.1 + image: autobase/console_db:2.5.2 platform: linux/amd64 environment: - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} @@ -35,7 +35,7 @@ services: retries: 10 autobase-api: - image: autobase/console_api:2.4.1 + image: autobase/console_api:2.5.2 platform: linux/amd64 environment: - PG_CONSOLE_DB_HOST=autobase-db From abd2df312c1a06654911d1d2873eae2a01bb0fed Mon Sep 17 00:00:00 2001 From: Dominic Schmid <45590457+dominic-schmid@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:06:30 +0100 Subject: [PATCH 05/51] fix(ui): hide already registered button when there are 0 users (#7918) --- resources/views/auth/register.blade.php | 28 +++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index cdfa52a98..0cf1a2772 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -73,21 +73,23 @@ $email = getOldOrLocal('email', 'test3@example.com'); -
-
-
+ @if (!$isFirstUser) +
+
+
+
+
+ + Already have an account? + +
-
- - Already have an account? - -
-
- - {{ __('auth.already_registered') }} - + + {{ __('auth.already_registered') }} + + @endif
From a0c307ac450bf8544fef936fec99875f35bc5482 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:18:36 +0100 Subject: [PATCH 06/51] fix(api): add custom_network_aliases to allowed fields --- app/Http/Controllers/Api/ApplicationsController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 0ec63562e..ca782651a 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -955,7 +955,7 @@ class ApplicationsController extends Controller if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } - $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', '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', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled']; + $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', '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', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled']; $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', @@ -2180,7 +2180,7 @@ class ApplicationsController extends Controller $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', '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_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']; $validationRules = [ 'name' => 'string|max:255', From 9c0e308dd35aae4ff06d4c7e3552f281e894c24e Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:26:11 +0100 Subject: [PATCH 07/51] refactor(api): make docker_compose_raw description more clear --- app/Http/Controllers/Api/ServicesController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index ec3d036bc..4025898b9 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -114,7 +114,7 @@ class ServicesController extends Controller 'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'], 'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'], 'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'], - 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose content.'], ], ), ), @@ -621,7 +621,7 @@ class ServicesController extends Controller 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'], 'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'], - 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose content.'], ], ) ), From ca2d02d8a344a039a8fc8863a78831bd6aa4ac52 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:31:06 +0100 Subject: [PATCH 08/51] chore(api): update openapi json and yaml --- openapi.json | 96 +++------------------------------------------------- openapi.yaml | 7 ++-- 2 files changed, 7 insertions(+), 96 deletions(-) diff --git a/openapi.json b/openapi.json index 5a2465a10..9c31f7628 100644 --- a/openapi.json +++ b/openapi.json @@ -8809,96 +8809,8 @@ ], "properties": { "type": { - "description": "The one-click service type", - "type": "string", - "enum": [ - "activepieces", - "appsmith", - "appwrite", - "authentik", - "babybuddy", - "budge", - "changedetection", - "chatwoot", - "classicpress-with-mariadb", - "classicpress-with-mysql", - "classicpress-without-database", - "cloudflared", - "code-server", - "dashboard", - "directus", - "directus-with-postgresql", - "docker-registry", - "docuseal", - "docuseal-with-postgres", - "dokuwiki", - "duplicati", - "emby", - "embystat", - "fider", - "filebrowser", - "firefly", - "formbricks", - "ghost", - "gitea", - "gitea-with-mariadb", - "gitea-with-mysql", - "gitea-with-postgresql", - "glance", - "glances", - "glitchtip", - "grafana", - "grafana-with-postgresql", - "grocy", - "heimdall", - "homepage", - "jellyfin", - "kuzzle", - "listmonk", - "logto", - "mediawiki", - "meilisearch", - "metabase", - "metube", - "minio", - "moodle", - "n8n", - "n8n-with-postgresql", - "next-image-transformation", - "nextcloud", - "nocodb", - "odoo", - "openblocks", - "pairdrop", - "penpot", - "phpmyadmin", - "pocketbase", - "posthog", - "reactive-resume", - "rocketchat", - "shlink", - "slash", - "snapdrop", - "statusnook", - "stirling-pdf", - "supabase", - "syncthing", - "tolgee", - "trigger", - "trigger-with-external-database", - "twenty", - "umami", - "unleash-with-postgresql", - "unleash-without-database", - "uptime-kuma", - "vaultwarden", - "vikunja", - "weblate", - "whoogle", - "wordpress-with-mariadb", - "wordpress-with-mysql", - "wordpress-without-database" - ] + "description": "The one-click service type (e.g. \"actualbudget\", \"calibre-web\", \"gitea-with-mysql\" ...)", + "type": "string" }, "name": { "type": "string", @@ -8937,7 +8849,7 @@ }, "docker_compose_raw": { "type": "string", - "description": "The Docker Compose raw content." + "description": "The base64 encoded Docker Compose content." } }, "type": "object" @@ -9187,7 +9099,7 @@ }, "docker_compose_raw": { "type": "string", - "description": "The Docker Compose raw content." + "description": "The base64 encoded Docker Compose content." } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 28aba508f..198322328 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5581,9 +5581,8 @@ paths: - environment_uuid properties: type: - description: 'The one-click service type' + description: 'The one-click service type (e.g. "actualbudget", "calibre-web", "gitea-with-mysql" ...)' type: string - enum: [activepieces, appsmith, appwrite, authentik, babybuddy, budge, changedetection, chatwoot, classicpress-with-mariadb, classicpress-with-mysql, classicpress-without-database, cloudflared, code-server, dashboard, directus, directus-with-postgresql, docker-registry, docuseal, docuseal-with-postgres, dokuwiki, duplicati, emby, embystat, fider, filebrowser, firefly, formbricks, ghost, gitea, gitea-with-mariadb, gitea-with-mysql, gitea-with-postgresql, glance, glances, glitchtip, grafana, grafana-with-postgresql, grocy, heimdall, homepage, jellyfin, kuzzle, listmonk, logto, mediawiki, meilisearch, metabase, metube, minio, moodle, n8n, n8n-with-postgresql, next-image-transformation, nextcloud, nocodb, odoo, openblocks, pairdrop, penpot, phpmyadmin, pocketbase, posthog, reactive-resume, rocketchat, shlink, slash, snapdrop, statusnook, stirling-pdf, supabase, syncthing, tolgee, trigger, trigger-with-external-database, twenty, umami, unleash-with-postgresql, unleash-without-database, uptime-kuma, vaultwarden, vikunja, weblate, whoogle, wordpress-with-mariadb, wordpress-with-mysql, wordpress-without-database] name: type: string maxLength: 255 @@ -5613,7 +5612,7 @@ paths: description: 'Start the service immediately after creation.' docker_compose_raw: type: string - description: 'The Docker Compose raw content.' + description: 'The base64 encoded Docker Compose content.' type: object responses: '201': @@ -5780,7 +5779,7 @@ paths: description: 'Connect the service to the predefined docker network.' docker_compose_raw: type: string - description: 'The Docker Compose raw content.' + description: 'The base64 encoded Docker Compose content.' type: object responses: '200': 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 09/51] 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 @@ class ApplicationsController extends Controller } #[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 @@ class ApplicationsController extends Controller } #[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 @@ class ApplicationsController extends Controller $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 10/51] 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 @@ class ApplicationsController extends Controller 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::group([ 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 11/51] 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\ValidGitBranch; 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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller $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 @@ class ApplicationsController extends Controller $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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller $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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller $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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller '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 @@ class ApplicationsController extends Controller $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 @@ class ApplicationsController extends Controller $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 12/51] 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.' From 764d8861f6ef0755d07b5c9c22ada2ae63993954 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:19:09 +0100 Subject: [PATCH 13/51] feat(api): add update urls support to services api - added update urls support to services api - remove old stale domains update code --- .../Controllers/Api/ServicesController.php | 192 ++++++++++++++---- openapi.json | 34 ++++ openapi.yaml | 8 + 3 files changed, 195 insertions(+), 39 deletions(-) diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 4025898b9..56812c94c 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -12,6 +12,7 @@ use App\Models\Project; use App\Models\Server; use App\Models\Service; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; use OpenApi\Attributes as OA; use Symfony\Component\Yaml\Yaml; @@ -37,6 +38,67 @@ class ServicesController extends Controller return serializeApiResponse($service); } + private function applyServiceUrls(Service $service, array $urls, string $teamId): ?array + { + $errors = []; + + foreach ($urls as $item) { + $name = data_get($item, 'name'); + $urls = data_get($item, 'url'); + + if (blank($name)) { + $errors[] = 'Service container name is required to apply URLs.'; + + continue; + } + + $application = $service->applications()->where('name', $name)->first(); + if (! $application) { + $errors[] = "Service container with '{$name}' not found."; + + continue; + } + + if (filled($urls)) { + $urls = str($urls)->replaceStart(',', '')->replaceEnd(',', '')->trim(); + $urls = str($urls)->explode(',')->map(function ($url) use (&$errors) { + $url = trim($url); + if (! filter_var($url, FILTER_VALIDATE_URL)) { + $errors[] = 'Invalid URL: '.$url; + + return str($url)->lower(); + } + $scheme = parse_url($url, PHP_URL_SCHEME) ?? ''; + if (! in_array(strtolower($scheme), ['http', 'https'])) { + $errors[] = "Invalid URL scheme: {$scheme} for URL: {$url}. Only http and https are supported."; + } + + return str($url)->lower(); + })->filter(fn ($u) => $u->isNotEmpty())->unique()->implode(','); + + if ($urls && empty($errors)) { + $result = checkIfDomainIsAlreadyUsedViaAPI(collect(explode(',', $urls)), $teamId, $application->uuid); + if ($result['hasConflicts']) { + foreach ($result['conflicts'] as $conflict) { + $errors[] = $conflict['message']; + } + } + } + } else { + $urls = null; + } + + $application->fqdn = $urls; + $application->save(); + } + + if (! empty($errors)) { + return ['errors' => $errors]; + } + + return null; + } + #[OA\Get( summary: 'List', description: 'List all services.', @@ -115,6 +177,17 @@ class ServicesController extends Controller 'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'], 'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'], 'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose content.'], + 'urls' => [ + 'type' => 'array', + 'description' => 'Array of URLs to be applied to containers of a service.', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'], + 'url' => ['type' => 'string', 'description' => 'Comma-separated list of domains (e.g. "http://app.coolify.io,https://app2.coolify.io").'], + ], + ), + ], ], ), ), @@ -152,7 +225,7 @@ class ServicesController extends Controller )] public function create_service(Request $request) { - $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw']; + $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'urls']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -165,7 +238,7 @@ class ServicesController extends Controller if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; } - $validator = customApiValidator($request->all(), [ + $validationRules = [ 'type' => 'string|required_without:docker_compose_raw', 'docker_compose_raw' => 'string|required_without:type', 'project_uuid' => 'string|required', @@ -176,7 +249,15 @@ class ServicesController extends Controller 'name' => 'string|max:255', 'description' => 'string|nullable', 'instant_deploy' => 'boolean', - ]); + 'urls' => 'array|nullable', + 'urls.*' => 'array:name,url', + 'urls.*.name' => 'string|required', + 'urls.*.url' => 'string|nullable', + ]; + $validationMessages = [ + 'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.', + ]; + $validator = Validator::make($request->all(), $validationRules, $validationMessages); $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -296,29 +377,31 @@ class ServicesController extends Controller // Apply service-specific application prerequisites applyServiceApplicationPrerequisites($service); + if ($request->has('urls') && is_array($request->urls)) { + $urlResult = $this->applyServiceUrls($service, $request->urls, $teamId); + if ($urlResult !== null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $urlResult['errors'], + ], 422); + } + } + if ($instantDeploy) { StartService::dispatch($service); } - $domains = $service->applications()->get()->pluck('fqdn')->sort(); - $domains = $domains->map(function ($domain) { - if (count(explode(':', $domain)) > 2) { - return str($domain)->beforeLast(':')->value(); - } - - return $domain; - }); return response()->json([ 'uuid' => $service->uuid, - 'domains' => $domains, - ]); + 'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(), + ])->setStatusCode(201); } return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404); } elseif (filled($request->docker_compose_raw)) { - $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; + $allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls']; - $validator = customApiValidator($request->all(), [ + $validationRules = [ 'project_uuid' => 'string|required', 'environment_name' => 'string|nullable', 'environment_uuid' => 'string|nullable', @@ -329,7 +412,15 @@ class ServicesController extends Controller 'instant_deploy' => 'boolean', 'connect_to_docker_network' => 'boolean', 'docker_compose_raw' => 'string|required', - ]); + 'urls' => 'array|nullable', + 'urls.*' => 'array:name,url', + 'urls.*.name' => 'string|required', + 'urls.*.url' => 'string|nullable', + ]; + $validationMessages = [ + 'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.', + ]; + $validator = Validator::make($request->all(), $validationRules, $validationMessages); $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -423,22 +514,24 @@ class ServicesController extends Controller $service->save(); $service->parse(isNew: true); + + if ($request->has('urls') && is_array($request->urls)) { + $urlResult = $this->applyServiceUrls($service, $request->urls, $teamId); + if ($urlResult !== null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $urlResult['errors'], + ], 422); + } + } + if ($instantDeploy) { StartService::dispatch($service); } - $domains = $service->applications()->get()->pluck('fqdn')->sort(); - $domains = $domains->map(function ($domain) { - if (count(explode(':', $domain)) > 2) { - return str($domain)->beforeLast(':')->value(); - } - - return $domain; - })->values(); - return response()->json([ 'uuid' => $service->uuid, - 'domains' => $domains, + 'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(), ])->setStatusCode(201); } elseif (filled($request->type)) { return response()->json([ @@ -622,6 +715,17 @@ class ServicesController extends Controller 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the service should be deployed instantly.'], 'connect_to_docker_network' => ['type' => 'boolean', 'default' => false, 'description' => 'Connect the service to the predefined docker network.'], 'docker_compose_raw' => ['type' => 'string', 'description' => 'The base64 encoded Docker Compose content.'], + 'urls' => [ + 'type' => 'array', + 'description' => 'Array of URLs to be applied to containers of a service.', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'], + 'url' => ['type' => 'string', 'description' => 'Comma-separated list of domains (e.g. "http://app.coolify.io,https://app2.coolify.io").'], + ], + ), + ], ], ) ), @@ -681,15 +785,23 @@ class ServicesController extends Controller $this->authorize('update', $service); - $allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network']; + $allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls']; - $validator = customApiValidator($request->all(), [ + $validationRules = [ 'name' => 'string|max:255', 'description' => 'string|nullable', 'instant_deploy' => 'boolean', 'connect_to_docker_network' => 'boolean', 'docker_compose_raw' => 'string|nullable', - ]); + 'urls' => 'array|nullable', + 'urls.*' => 'array:name,url', + 'urls.*.name' => 'string|required', + 'urls.*.url' => 'string|nullable', + ]; + $validationMessages = [ + 'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.', + ]; + $validator = Validator::make($request->all(), $validationRules, $validationMessages); $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -753,22 +865,24 @@ class ServicesController extends Controller $service->save(); $service->parse(); + + if ($request->has('urls') && is_array($request->urls)) { + $urlResult = $this->applyServiceUrls($service, $request->urls, $teamId); + if ($urlResult !== null) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $urlResult['errors'], + ], 422); + } + } + if ($request->instant_deploy) { StartService::dispatch($service); } - $domains = $service->applications()->get()->pluck('fqdn')->sort(); - $domains = $domains->map(function ($domain) { - if (count(explode(':', $domain)) > 2) { - return str($domain)->beforeLast(':')->value(); - } - - return $domain; - })->values(); - return response()->json([ 'uuid' => $service->uuid, - 'domains' => $domains, + 'domains' => $service->applications()->pluck('fqdn')->filter()->sort()->values(), ])->setStatusCode(200); } diff --git a/openapi.json b/openapi.json index 9fdec634f..e9207e5b9 100644 --- a/openapi.json +++ b/openapi.json @@ -8887,6 +8887,23 @@ "docker_compose_raw": { "type": "string", "description": "The base64 encoded Docker Compose content." + }, + "urls": { + "type": "array", + "description": "Array of URLs to be applied to containers of a service.", + "items": { + "properties": { + "name": { + "type": "string", + "description": "The service name as defined in docker-compose." + }, + "url": { + "type": "string", + "description": "Comma-separated list of domains (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")." + } + }, + "type": "object" + } } }, "type": "object" @@ -9137,6 +9154,23 @@ "docker_compose_raw": { "type": "string", "description": "The base64 encoded Docker Compose content." + }, + "urls": { + "type": "array", + "description": "Array of URLs to be applied to containers of a service.", + "items": { + "properties": { + "name": { + "type": "string", + "description": "The service name as defined in docker-compose." + }, + "url": { + "type": "string", + "description": "Comma-separated list of domains (e.g. \"http:\/\/app.coolify.io,https:\/\/app2.coolify.io\")." + } + }, + "type": "object" + } } }, "type": "object" diff --git a/openapi.yaml b/openapi.yaml index 383481a8d..735ebad21 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5606,6 +5606,10 @@ paths: docker_compose_raw: type: string description: 'The base64 encoded Docker Compose content.' + urls: + type: array + description: 'Array of URLs to be applied to containers of a service.' + items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, url: { type: string, description: 'Comma-separated list of domains (e.g. "http://app.coolify.io,https://app2.coolify.io").' } }, type: object } type: object responses: '201': @@ -5773,6 +5777,10 @@ paths: docker_compose_raw: type: string description: 'The base64 encoded Docker Compose content.' + urls: + type: array + description: 'Array of URLs to be applied to containers of a service.' + items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, url: { type: string, description: 'Comma-separated list of domains (e.g. "http://app.coolify.io,https://app2.coolify.io").' } }, type: object } type: object responses: '200': From cd1c333a6f2235767a329c4aa21aa53aad4729d6 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:51:57 +1100 Subject: [PATCH 14/51] feat(service): add chibisafe template (#5808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🏔️ Peak <122374094+peaklabs-dev@users.noreply.github.com> Co-authored-by: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> --- public/svgs/chibisafe.svg | 90 ++++++++++++++++++++++++++++++++ templates/compose/chibisafe.yaml | 80 ++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 public/svgs/chibisafe.svg create mode 100644 templates/compose/chibisafe.yaml diff --git a/public/svgs/chibisafe.svg b/public/svgs/chibisafe.svg new file mode 100644 index 000000000..03fc3709a --- /dev/null +++ b/public/svgs/chibisafe.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/compose/chibisafe.yaml b/templates/compose/chibisafe.yaml new file mode 100644 index 000000000..62ced7a80 --- /dev/null +++ b/templates/compose/chibisafe.yaml @@ -0,0 +1,80 @@ +# documentation: https://chibisafe.app/docs/intro +# slogan: A beautiful and performant vault to save all your files in the cloud. +# tags: storage,file-sharing,upload,sharing +# logo: svgs/chibisafe.svg +# port: 80 + +## This service includes its own reverse proxy because the application routing is broken, also it needs caddy for serving static files https://github.com/coollabsio/coolify/pull/5808#pullrequestreview-3340419006 + +services: + chibisafe: + image: chibisafe/chibisafe:v6.5.5 + environment: + - BASE_API_URL=http://chibisafe-server:8000 + healthcheck: + test: ["CMD", "wget", "--spider", "--quiet", "http://127.0.0.1:8001"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + chibisafe-server: + image: chibisafe/chibisafe-server:v6.5.5 + volumes: + - chibisafe-database:/app/database:rw + - chibisafe-uploads:/app/uploads:rw + - chibisafe-logs:/app/logs:rw + healthcheck: + test: + [ + "CMD", + "wget", + "--spider", + "--quiet", + "http://127.0.0.1:8000/api/health", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + caddy: + image: caddy:2-alpine + environment: + - SERVICE_URL_CHIBISAFE_80 + healthcheck: + test: ["CMD", "wget", "--spider", "--quiet", "http://127.0.0.1:80"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + volumes: + - uploads:/app/uploads:ro + - type: bind + source: ./Caddyfile + target: /etc/caddy/Caddyfile + content: | + # chibisafe/Caddyfile + # https://chibisafe.moe/guides/running-with-docker + :80 { + route { + file_server * { + root /app/uploads + pass_thru + } + @api path /api/* + reverse_proxy @api http://chibisafe-server:8000 { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Real-IP {http.request.header.X-Real-IP} + } + @docs path /docs* + reverse_proxy @docs http://chibisafe-server:8000 { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Real-IP {http.request.header.X-Real-IP} + } + reverse_proxy http://chibisafe:8001 { + header_up Host {http.reverse_proxy.upstream.hostport} + header_up X-Real-IP {http.request.header.X-Real-IP} + } + } + } From d4957ff3d1146c005c7d57bd283e31ac933daed3 Mon Sep 17 00:00:00 2001 From: Dominic Schmid <45590457+dominic-schmid@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:09:56 +0100 Subject: [PATCH 15/51] feat(ui): improve sidebar menu items styling (#7928) --- resources/css/utilities.css | 25 +++++- resources/views/components/navbar.blade.php | 82 +++++++++---------- .../components/server/sidebar-proxy.blade.php | 14 ++-- .../server/sidebar-security.blade.php | 10 +-- .../views/components/server/sidebar.blade.php | 56 ++++++------- .../service-database/sidebar.blade.php | 21 +++-- .../components/settings/sidebar.blade.php | 14 ++-- resources/views/layouts/app.blade.php | 8 +- .../application/configuration.blade.php | 81 ++++++++---------- .../project/database/configuration.blade.php | 52 ++++++------ .../project/service/configuration.blade.php | 41 ++++------ .../livewire/project/service/index.blade.php | 11 ++- resources/views/livewire/upgrade.blade.php | 2 +- 13 files changed, 210 insertions(+), 207 deletions(-) diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 7978b2d19..c7b77022f 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -174,15 +174,34 @@ } @utility menu-item { - @apply flex gap-3 items-center px-2 py-1 w-full text-sm sm:pr-0 dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 min-w-fit sm:min-w-64; + @apply flex gap-3 items-center px-2 py-1 w-full text-sm dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 rounded-sm truncate min-w-0; +} +@utility menu-item-icon { + @apply flex-shrink-0 w-6 h-6 dark:hover:text-white; +} + +@utility menu-item-label { + @apply min-w-0 flex-1 truncate; } @utility menu-item-active { - @apply text-black rounded-none dark:bg-coolgray-200 dark:text-warning bg-neutral-200; + @apply text-black rounded-sm dark:bg-coolgray-200 dark:text-warning bg-neutral-200 overflow-hidden; +} + +@utility sub-menu-wrapper { + @apply flex flex-col items-start gap-2 min-w-40 sm:min-w-48 w-auto max-w-full sm:flex-shrink; +} + +@utility sub-menu-item { + @apply flex gap-2 items-center px-2 py-1 w-full text-sm dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 rounded-sm truncate min-w-0; +} + +@utility sub-menu-item-icon { + @apply flex-shrink-0 w-4 h-4 dark:hover:text-white; } @utility heading-item-active { - @apply text-black rounded-none dark:bg-coolgray-200 dark:text-warning; + @apply text-black rounded-sm dark:bg-coolgray-200 dark:text-warning; } @utility icon { diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index e351a4480..48b544ebb 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -62,11 +62,11 @@ html { font-size: 93.75%; } - + :root { --vh: 1vh; } - + @media (min-width: 1024px) { html { font-size: 87.5%; @@ -107,19 +107,19 @@
  • - - Dashboard + Dashboard
  • - @@ -127,14 +127,14 @@ - Projects + Projects
  • - @@ -145,7 +145,7 @@ - Servers + Servers
  • @@ -153,11 +153,11 @@ - + - Sources + Sources
  • @@ -165,19 +165,19 @@ class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}" href="{{ route('destination.index') }}"> - + - Destinations + Destinations
  • - + @@ -185,14 +185,14 @@ - S3 Storages + S3 Storages
  • - + - Shared Variables + Shared Variables
  • - + - Notifications + Notifications
  • - + - Keys & Tokens + Keys & Tokens
  • - + - Tags + Tags
  • @can('canAccessTerminal') @@ -247,14 +247,14 @@ - - Terminal + Terminal @endcan @@ -262,7 +262,7 @@ - @@ -270,14 +270,14 @@ - Profile + Profile
  • - @@ -288,7 +288,7 @@ - Teams + Teams
  • @if (isCloud() && auth()->user()->isAdmin()) @@ -296,12 +296,12 @@ - + - Subscription + Subscription @endif @@ -311,7 +311,7 @@ - @@ -319,7 +319,7 @@ d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" /> - Settings + Settings @endif @@ -328,12 +328,12 @@ @if (isInstanceAdmin() || session('impersonating'))
  • - - Admin + Admin
  • @endif @@ -350,7 +350,7 @@ - + @@ -360,7 +360,7 @@
  • - + @@ -368,7 +368,7 @@ d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" /> - Sponsor us + Sponsor us
  • @endif @@ -380,11 +380,11 @@ @@ -394,11 +394,11 @@
    @csrf
    diff --git a/resources/views/components/server/sidebar-proxy.blade.php b/resources/views/components/server/sidebar-proxy.blade.php index 5a76a2f1a..00bb3ee2a 100644 --- a/resources/views/components/server/sidebar-proxy.blade.php +++ b/resources/views/components/server/sidebar-proxy.blade.php @@ -1,16 +1,16 @@ - diff --git a/resources/views/components/server/sidebar-security.blade.php b/resources/views/components/server/sidebar-security.blade.php index 995d258cf..e61d23c89 100644 --- a/resources/views/components/server/sidebar-security.blade.php +++ b/resources/views/components/server/sidebar-security.blade.php @@ -1,10 +1,10 @@ - diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index e485ec545..2d7649fab 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -1,53 +1,51 @@ -
    - General + diff --git a/resources/views/components/service-database/sidebar.blade.php b/resources/views/components/service-database/sidebar.blade.php index 728df3a7b..9d31fc634 100644 --- a/resources/views/components/service-database/sidebar.blade.php +++ b/resources/views/components/service-database/sidebar.blade.php @@ -4,21 +4,24 @@ 'isImportSupported' => false, ]) -
    - + - + + + + Back - General + General @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated) - Backups + Backups @endif @if ($isImportSupported) - Import Backup + Import Backup @endif
    diff --git a/resources/views/components/settings/sidebar.blade.php b/resources/views/components/settings/sidebar.blade.php index 8b564880c..0a68a9155 100644 --- a/resources/views/components/settings/sidebar.blade.php +++ b/resources/views/components/settings/sidebar.blade.php @@ -1,8 +1,8 @@ -
    - General - Advanced - Updates + diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 0ef021458..93d6fe413 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -22,7 +22,7 @@