From ab63797db4864c3079dbcb82f8220bf4c04810e1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:08:07 +0200 Subject: [PATCH 01/16] chore(versions): update coolify version numbers to 4.0.0-beta.403 and 4.0.0-beta.404 --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index f35ae1635..fabc1ad85 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.402', + 'version' => '4.0.0-beta.403', 'helper_version' => '1.0.8', 'realtime_version' => '1.0.6', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 1ad2d31cf..22aec8cce 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.402" + "version": "4.0.0-beta.403" }, "nightly": { - "version": "4.0.0-beta.403" + "version": "4.0.0-beta.404" }, "helper": { "version": "1.0.7" diff --git a/versions.json b/versions.json index 5758b46d0..52d58882e 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.402" + "version": "4.0.0-beta.403" }, "nightly": { - "version": "4.0.0-beta.403" + "version": "4.0.0-beta.404" }, "helper": { "version": "1.0.8" From 7519e6a939ba51faf0f418e57df54f0ff38caff1 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:31:38 +0200 Subject: [PATCH 02/16] fix(ui): Instance Backup settings - Only show and enable instance backup settings when the server is functional --- app/Livewire/SettingsBackup.php | 7 +++ .../views/livewire/settings-backup.blade.php | 49 ++++++++++++------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 1b0599ffe..bb5ed0aa8 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -15,6 +15,8 @@ class SettingsBackup extends Component { public InstanceSettings $settings; + public Server $server; + public ?StandalonePostgresql $database = null; public ScheduledDatabaseBackup|null|array $backup = []; @@ -46,6 +48,7 @@ class SettingsBackup extends Component return redirect()->route('dashboard'); } else { $settings = instanceSettings(); + $this->server = Server::findOrFail(0); $this->database = StandalonePostgresql::whereName('coolify-db')->first(); $s3s = S3Storage::whereTeamId(0)->get() ?? []; if ($this->database) { @@ -60,6 +63,10 @@ class SettingsBackup extends Component $this->database->save(); } $this->backup = $this->database->scheduledBackups->first(); + if ($this->backup && ! $this->server->isFunctional()) { + $this->backup->enabled = false; + $this->backup->save(); + } $this->executions = $this->backup->executions; } $this->settings = $settings; diff --git a/resources/views/livewire/settings-backup.blade.php b/resources/views/livewire/settings-backup.blade.php index 9760c173d..045a1d368 100644 --- a/resources/views/livewire/settings-backup.blade.php +++ b/resources/views/livewire/settings-backup.blade.php @@ -6,7 +6,7 @@

Backup

- @if (isset($database)) + @if (isset($database) && $server->isFunctional()) Save @@ -14,26 +14,39 @@
Backup configuration for Coolify instance.
- @if (isset($database) && isset($backup)) -
-
- - - + @if ($server->isFunctional()) + @if (isset($database) && isset($backup)) +
+
+ + + +
+
+ + +
-
- - + +
+
-
- -
- -
+ @else + To configure automatic backup for your Coolify instance, you first need to add a database resource + into Coolify. + Configure Backup + @endif @else - To configure automatic backup for your Coolify instance, you first need to add a database resource - into Coolify. - Configure Backup +
+
+ Instance Backup is currently disabled because the localhost server is not properly validated. + Please validate your server to enable Instance Backup. +
+ + Go to Server Settings to Validate + +
@endif
From c6b7779c824e87b5a0683002d0cf330e7c76d625 Mon Sep 17 00:00:00 2001 From: Chingiz Mammadov Date: Tue, 1 Apr 2025 16:45:44 +0400 Subject: [PATCH 03/16] feat(lang): Added Azerbaijani language updated turkish language. (#5497) --- lang/az.json | 40 ++++++++++++++++++++++++++++++++++++++++ lang/tr.json | 10 +++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 lang/az.json diff --git a/lang/az.json b/lang/az.json new file mode 100644 index 000000000..973c70c2f --- /dev/null +++ b/lang/az.json @@ -0,0 +1,40 @@ +{ + "auth.login": "Daxil ol", + "auth.login.authentik": "Authentik ilə daxil ol", + "auth.login.azure": "Azure ilə daxil ol", + "auth.login.bitbucket": "Bitbucket ilə daxil ol", + "auth.login.github": "Github ilə daxil ol", + "auth.login.gitlab": "GitLab ilə daxil ol", + "auth.login.google": "Google ilə daxil ol", + "auth.login.infomaniak": "Infomaniak ilə daxil ol", + "auth.already_registered": "Qeytiyatınız var?", + "auth.confirm_password": "Şifrəni təsdiqləyin", + "auth.forgot_password": "Şifrəmi unutdum", + "auth.forgot_password_send_email": "Şifrəni sıfırlamaq üçün e-poçt göndər", + "auth.register_now": "Qeydiyyat", + "auth.logout": "Çıxış", + "auth.register": "Qeydiyyat", + "auth.registration_disabled": "Qeydiyyat bağlıdır. Administratorla əlaqə saxlayın.", + "auth.reset_password": "Şifrənin bərpası", + "auth.failed": "Bu məlumatlar bizim qeydlərimizlə uyğun gəlmir.", + "auth.failed.callback": "Giriş təminatçısından geri çağırma işlənə bilmədi.", + "auth.failed.password": "Daxil etdiyiniz şifrə yanlışdır.", + "auth.failed.email": "Bu e-poçt ünvanı ilə istifadəçi tapılmadı.", + "auth.throttle": "Çox sayda uğursuz giriş cəhdi. Zəhmət olmasa :seconds saniyə sonra yenidən cəhd edin.", + "input.name": "Ad", + "input.email": "E-poçt", + "input.password": "Şifrə", + "input.password.again": "Şifrəni təkrar daxil edin", + "input.code": "Bir dəfəlik kod", + "input.recovery_code": "Bərpa kodu", + "button.save": "Yadda saxla", + "repository.url": "Nümunələr
Publik repozitoriyalar üçün https://... istifadə edin.
Özəl repozitoriyalar üçün git@... istifadə edin.

https://github.com/coollabsio/coolify-examples main branch-ı seçiləcək
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch-ı seçiləcək.
https://gitea.com/sedlav/expressjs.git main branch-ı seçiləcək.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch-ı seçiləcək.", + "service.stop": "Bu xidmət dayandırılacaq.", + "resource.docker_cleanup": "Docker təmizlənməsini işə salın (istifadə olunmayan şəkillər və builder keşini silin).", + "resource.non_persistent": "Bütün qeyri-daimi məlumatlar silinəcək.", + "resource.delete_volumes": "Bu resursla əlaqəli bütün həcm məlumatları tamamilə silinəcək.", + "resource.delete_connected_networks": "Bu resursla əlaqəli bütün əvvəlcədən təyin olunmamış şəbəkələr tamamilə silinəcək.", + "resource.delete_configurations": "Serverdən bütün konfiqurasiya faylları tamamilə silinəcək.", + "database.delete_backups_locally": "Bütün ehtiyat nüsxələr lokal yaddaşdan tamamilə silinəcək.", + "warning.sslipdomain": "Konfiqurasiya yadda saxlanıldı, lakin sslip domeni ilə https TÖVSİYƏ EDİLMİR, çünki Let's Encrypt serverləri bu ümumi domenlə məhdudlaşdırılır (SSL sertifikatının təsdiqlənməsi uğursuz olacaq).

Əvəzində öz domeninizdən istifadə edin." +} diff --git a/lang/tr.json b/lang/tr.json index 3cbcee409..663c756f9 100644 --- a/lang/tr.json +++ b/lang/tr.json @@ -27,5 +27,13 @@ "input.code": "Tek Kullanımlık Kod", "input.recovery_code": "Kurtarma Kodu", "button.save": "Kaydet", - "repository.url": "Örnekler
Halka açık depolar için https://... kullanın.
Özel depolar için git@... kullanın.

https://github.com/coollabsio/coolify-examples main dalı seçilecek
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify dalı seçilecek.
https://gitea.com/sedlav/expressjs.git main dalı seçilecek.
https://gitlab.com/andrasbacsai/nodejs-example.git main dalı seçilecek." + "repository.url": "Örnekler
Halka açık depolar için https://... kullanın.
Özel depolar için git@... kullanın.

https://github.com/coollabsio/coolify-examples main dalı seçilecek
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify dalı seçilecek.
https://gitea.com/sedlav/expressjs.git main dalı seçilecek.
https://gitlab.com/andrasbacsai/nodejs-example.git main dalı seçilecek.", + "service.stop": "Bu servis durdurulacak.", + "resource.docker_cleanup": "Docker temizliği çalıştır (kullanılmayan imajları ve oluşturucu önbelleğini kaldır).", + "resource.non_persistent": "Tüm kalıcı olmayan veriler silinecek.", + "resource.delete_volumes": "Bu kaynakla ilişkili tüm hacimler kalıcı olarak silinecek.", + "resource.delete_connected_networks": "Bu kaynakla ilişkili önceden tanımlanmamış tüm ağlar kalıcı olarak silinecek.", + "resource.delete_configurations": "Sunucudaki tüm yapılandırma dosyaları kalıcı olarak silinecek.", + "database.delete_backups_locally": "Tüm yedekler yerel depolamadan kalıcı olarak silinecek.", + "warning.sslipdomain": "Yapılandırmanız kaydedildi, ancak sslip domain ile https ÖNERİLMEZ, çünkü Let's Encrypt sunucuları bu genel domain ile sınırlandırılmıştır (SSL sertifikası doğrulaması başarısız olur).

Bunun yerine kendi domaininizi kullanın." } From 1384de7566df186c90e7c0c9030fa4289b1d3931 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Tue, 1 Apr 2025 20:57:20 +0200 Subject: [PATCH 04/16] fix(docs): comment out execute for now - Due to security concerns, execute is disabled, so we need to comment out the code as well to update the docs. --- .../Api/ApplicationsController.php | 368 +++++++++--------- openapi.json | 74 ---- openapi.yaml | 46 --- 3 files changed, 184 insertions(+), 304 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index cbeb6b55d..0044e86b4 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -2881,198 +2881,198 @@ class ApplicationsController extends Controller ); } - #[OA\Post( - summary: 'Execute Command', - description: "Execute a command on the application's current container.", - path: '/applications/{uuid}/execute', - operationId: 'execute-command-application', - security: [ - ['bearerAuth' => []], - ], - tags: ['Applications'], - parameters: [ - new OA\Parameter( - name: 'uuid', - in: 'path', - description: 'UUID of the application.', - required: true, - schema: new OA\Schema( - type: 'string', - format: 'uuid', - ) - ), - ], - requestBody: new OA\RequestBody( - required: true, - description: 'Command to execute.', - content: new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - type: 'object', - properties: [ - 'command' => ['type' => 'string', 'description' => 'Command to execute.'], - ], - ), - ), - ), - responses: [ - new OA\Response( - response: 200, - description: "Execute a command on the application's current container.", - content: [ - new OA\MediaType( - mediaType: 'application/json', - schema: new OA\Schema( - type: 'object', - properties: [ - 'message' => ['type' => 'string', 'example' => 'Command executed.'], - 'response' => ['type' => 'string'], - ] - ) - ), - ] - ), - new OA\Response( - response: 401, - ref: '#/components/responses/401', - ), - new OA\Response( - response: 400, - ref: '#/components/responses/400', - ), - new OA\Response( - response: 404, - ref: '#/components/responses/404', - ), - ] - )] - public function execute_command_by_uuid(Request $request) - { - // TODO: Need to review this from security perspective, to not allow arbitrary command execution - $allowedFields = ['command']; - $teamId = getTeamIdFromToken(); - if (is_null($teamId)) { - return invalidTokenResponse(); - } - $uuid = $request->route('uuid'); - if (! $uuid) { - return response()->json(['message' => 'UUID is required.'], 400); - } - $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); - if (! $application) { - return response()->json(['message' => 'Application not found.'], 404); - } - $return = validateIncomingRequest($request); - if ($return instanceof \Illuminate\Http\JsonResponse) { - return $return; - } - $validator = customApiValidator($request->all(), [ - 'command' => 'string|required', - ]); + // #[OA\Post( + // summary: 'Execute Command', + // description: "Execute a command on the application's current container.", + // path: '/applications/{uuid}/execute', + // operationId: 'execute-command-application', + // security: [ + // ['bearerAuth' => []], + // ], + // tags: ['Applications'], + // parameters: [ + // new OA\Parameter( + // name: 'uuid', + // in: 'path', + // description: 'UUID of the application.', + // required: true, + // schema: new OA\Schema( + // type: 'string', + // format: 'uuid', + // ) + // ), + // ], + // requestBody: new OA\RequestBody( + // required: true, + // description: 'Command to execute.', + // content: new OA\MediaType( + // mediaType: 'application/json', + // schema: new OA\Schema( + // type: 'object', + // properties: [ + // 'command' => ['type' => 'string', 'description' => 'Command to execute.'], + // ], + // ), + // ), + // ), + // responses: [ + // new OA\Response( + // response: 200, + // description: "Execute a command on the application's current container.", + // content: [ + // new OA\MediaType( + // mediaType: 'application/json', + // schema: new OA\Schema( + // type: 'object', + // properties: [ + // 'message' => ['type' => 'string', 'example' => 'Command executed.'], + // 'response' => ['type' => 'string'], + // ] + // ) + // ), + // ] + // ), + // new OA\Response( + // response: 401, + // ref: '#/components/responses/401', + // ), + // new OA\Response( + // response: 400, + // ref: '#/components/responses/400', + // ), + // new OA\Response( + // response: 404, + // ref: '#/components/responses/404', + // ), + // ] + // )] + // public function execute_command_by_uuid(Request $request) + // { + // // TODO: Need to review this from security perspective, to not allow arbitrary command execution + // $allowedFields = ['command']; + // $teamId = getTeamIdFromToken(); + // if (is_null($teamId)) { + // return invalidTokenResponse(); + // } + // $uuid = $request->route('uuid'); + // if (! $uuid) { + // return response()->json(['message' => 'UUID is required.'], 400); + // } + // $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + // if (! $application) { + // return response()->json(['message' => 'Application not found.'], 404); + // } + // $return = validateIncomingRequest($request); + // if ($return instanceof \Illuminate\Http\JsonResponse) { + // return $return; + // } + // $validator = customApiValidator($request->all(), [ + // 'command' => 'string|required', + // ]); - $extraFields = array_diff(array_keys($request->all()), $allowedFields); - if ($validator->fails() || ! empty($extraFields)) { - $errors = $validator->errors(); - if (! empty($extraFields)) { - foreach ($extraFields as $field) { - $errors->add($field, 'This field is not allowed.'); - } - } + // $extraFields = array_diff(array_keys($request->all()), $allowedFields); + // if ($validator->fails() || ! empty($extraFields)) { + // $errors = $validator->errors(); + // if (! empty($extraFields)) { + // foreach ($extraFields as $field) { + // $errors->add($field, 'This field is not allowed.'); + // } + // } - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } + // return response()->json([ + // 'message' => 'Validation failed.', + // 'errors' => $errors, + // ], 422); + // } - $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail(); - $status = getContainerStatus($application->destination->server, $container['Names']); + // $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail(); + // $status = getContainerStatus($application->destination->server, $container['Names']); - if ($status !== 'running') { - return response()->json([ - 'message' => 'Application is not running.', - ], 400); - } + // if ($status !== 'running') { + // return response()->json([ + // 'message' => 'Application is not running.', + // ], 400); + // } - $commands = collect([ - executeInDocker($container['Names'], $request->command), - ]); + // $commands = collect([ + // executeInDocker($container['Names'], $request->command), + // ]); - $res = instant_remote_process(command: $commands, server: $application->destination->server); + // $res = instant_remote_process(command: $commands, server: $application->destination->server); - return response()->json([ - 'message' => 'Command executed.', - 'response' => $res, - ]); - } + // return response()->json([ + // 'message' => 'Command executed.', + // 'response' => $res, + // ]); + // } - private function validateDataApplications(Request $request, Server $server) - { - $teamId = getTeamIdFromToken(); + // private function validateDataApplications(Request $request, Server $server) + // { + // $teamId = getTeamIdFromToken(); - // Validate ports_mappings - if ($request->has('ports_mappings')) { - $ports = []; - foreach (explode(',', $request->ports_mappings) as $portMapping) { - $port = explode(':', $portMapping); - if (in_array($port[0], $ports)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'ports_mappings' => 'The first number before : should be unique between mappings.', - ], - ], 422); - } - $ports[] = $port[0]; - } - } - // Validate custom_labels - if ($request->has('custom_labels')) { - if (! isBase64Encoded($request->custom_labels)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - } - $customLabels = base64_decode($request->custom_labels); - if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'custom_labels' => 'The custom_labels should be base64 encoded.', - ], - ], 422); - } - } - if ($request->has('domains') && $server->isProxyShouldRun()) { - $uuid = $request->uuid; - $fqdn = $request->domains; - $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); - $fqdn = str($fqdn)->replaceStart(',', '')->trim(); - $errors = []; - $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { - if (filter_var($domain, FILTER_VALIDATE_URL) === false) { - $errors[] = 'Invalid domain: '.$domain; - } + // // Validate ports_mappings + // if ($request->has('ports_mappings')) { + // $ports = []; + // foreach (explode(',', $request->ports_mappings) as $portMapping) { + // $port = explode(':', $portMapping); + // if (in_array($port[0], $ports)) { + // return response()->json([ + // 'message' => 'Validation failed.', + // 'errors' => [ + // 'ports_mappings' => 'The first number before : should be unique between mappings.', + // ], + // ], 422); + // } + // $ports[] = $port[0]; + // } + // } + // // Validate custom_labels + // if ($request->has('custom_labels')) { + // if (! isBase64Encoded($request->custom_labels)) { + // return response()->json([ + // 'message' => 'Validation failed.', + // 'errors' => [ + // 'custom_labels' => 'The custom_labels should be base64 encoded.', + // ], + // ], 422); + // } + // $customLabels = base64_decode($request->custom_labels); + // if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + // return response()->json([ + // 'message' => 'Validation failed.', + // 'errors' => [ + // 'custom_labels' => 'The custom_labels should be base64 encoded.', + // ], + // ], 422); + // } + // } + // if ($request->has('domains') && $server->isProxyShouldRun()) { + // $uuid = $request->uuid; + // $fqdn = $request->domains; + // $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + // $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + // $errors = []; + // $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + // if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + // $errors[] = 'Invalid domain: '.$domain; + // } - return str($domain)->trim()->lower(); - }); - if (count($errors) > 0) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => $errors, - ], 422); - } - if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { - return response()->json([ - 'message' => 'Validation failed.', - 'errors' => [ - 'domains' => 'One of the domain is already used.', - ], - ], 422); - } - } - } + // return str($domain)->trim()->lower(); + // }); + // if (count($errors) > 0) { + // return response()->json([ + // 'message' => 'Validation failed.', + // 'errors' => $errors, + // ], 422); + // } + // if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + // return response()->json([ + // 'message' => 'Validation failed.', + // 'errors' => [ + // 'domains' => 'One of the domain is already used.', + // ], + // ], 422); + // } + // } + // } } diff --git a/openapi.json b/openapi.json index 98447067e..dbbc3dc24 100644 --- a/openapi.json +++ b/openapi.json @@ -2771,80 +2771,6 @@ ] } }, - "\/applications\/{uuid}\/execute": { - "post": { - "tags": [ - "Applications" - ], - "summary": "Execute Command", - "description": "Execute a command on the application's current container.", - "operationId": "execute-command-application", - "parameters": [ - { - "name": "uuid", - "in": "path", - "description": "UUID of the application.", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "description": "Command to execute.", - "required": true, - "content": { - "application\/json": { - "schema": { - "properties": { - "command": { - "type": "string", - "description": "Command to execute." - } - }, - "type": "object" - } - } - } - }, - "responses": { - "200": { - "description": "Execute a command on the application's current container.", - "content": { - "application\/json": { - "schema": { - "properties": { - "message": { - "type": "string", - "example": "Command executed." - }, - "response": { - "type": "string" - } - }, - "type": "object" - } - } - } - }, - "401": { - "$ref": "#\/components\/responses\/401" - }, - "400": { - "$ref": "#\/components\/responses\/400" - }, - "404": { - "$ref": "#\/components\/responses\/404" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, "\/databases": { "get": { "tags": [ diff --git a/openapi.yaml b/openapi.yaml index ba4b7193e..b8f34ef19 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1905,52 +1905,6 @@ paths: security: - bearerAuth: [] - '/applications/{uuid}/execute': - post: - tags: - - Applications - summary: 'Execute Command' - description: "Execute a command on the application's current container." - operationId: execute-command-application - parameters: - - - name: uuid - in: path - description: 'UUID of the application.' - required: true - schema: - type: string - format: uuid - requestBody: - description: 'Command to execute.' - required: true - content: - application/json: - schema: - properties: - command: - type: string - description: 'Command to execute.' - type: object - responses: - '200': - description: "Execute a command on the application's current container." - content: - application/json: - schema: - properties: - message: { type: string, example: 'Command executed.' } - response: { type: string } - type: object - '401': - $ref: '#/components/responses/401' - '400': - $ref: '#/components/responses/400' - '404': - $ref: '#/components/responses/404' - security: - - - bearerAuth: [] /databases: get: tags: From 1016b000c6c890280ecafb4e936dab8eb94f315a Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:28:00 +0200 Subject: [PATCH 05/16] fix(installation): mount the docker config - Mount docker config during installation to use docker credentials, if there are any. --- scripts/upgrade.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index e59f4d138..57c3a09a2 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -39,9 +39,15 @@ if ! docker network inspect coolify >/dev/null 2>&1; then fi # docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null +# Check if Docker config file exists +DOCKER_CONFIG_MOUNT="" +if [ -f /etc/docker/config.json ]; then + DOCKER_CONFIG_MOUNT="-v /etc/docker/config.json:/root/.docker/config.json" +fi + if [ -f /data/coolify/source/docker-compose.custom.yml ]; then echo "docker-compose.custom.yml detected." >>$LOGFILE - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1 else - docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1 + docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock ${DOCKER_CONFIG_MOUNT} --rm ${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >>$LOGFILE 2>&1 fi From f53f67165655c27072cb4f1a2ead3726f459a778 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:35:37 +0200 Subject: [PATCH 06/16] fix(installation): path to config file for docker login --- scripts/upgrade.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 57c3a09a2..32bffad48 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -41,8 +41,8 @@ fi # Check if Docker config file exists DOCKER_CONFIG_MOUNT="" -if [ -f /etc/docker/config.json ]; then - DOCKER_CONFIG_MOUNT="-v /etc/docker/config.json:/root/.docker/config.json" +if [ -f /root/.docker/config.json ]; then + DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json" fi if [ -f /data/coolify/source/docker-compose.custom.yml ]; then From 2f6bd04f6bf65781d29c4b0243b864393057b97c Mon Sep 17 00:00:00 2001 From: Elton Ciatto <53355283+eltonciatto@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:36:57 -0300 Subject: [PATCH 07/16] feat(lang): Added Portuguese from Brazil language (#5500) --- lang/pt-br.json | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lang/pt-br.json diff --git a/lang/pt-br.json b/lang/pt-br.json new file mode 100644 index 000000000..2e793890f --- /dev/null +++ b/lang/pt-br.json @@ -0,0 +1,40 @@ +{ + "auth.login": "Entrar", + "auth.login.authentik": "Entrar com Authentik", + "auth.login.azure": "Entrar com Microsoft", + "auth.login.bitbucket": "Entrar com Bitbucket", + "auth.login.github": "Entrar com GitHub", + "auth.login.gitlab": "Entrar com Gitlab", + "auth.login.google": "Entrar com Google", + "auth.login.infomaniak": "Entrar com Infomaniak", + "auth.already_registered": "Já tem uma conta?", + "auth.confirm_password": "Confirmar senha", + "auth.forgot_password": "Esqueceu a senha", + "auth.forgot_password_send_email": "Enviar e-mail para redefinir senha", + "auth.register_now": "Cadastre-se", + "auth.logout": "Sair", + "auth.register": "Cadastrar", + "auth.registration_disabled": "O registro está desativado. Por favor, contate o administrador.", + "auth.reset_password": "Redefinir senha", + "auth.failed": "Essas credenciais não correspondem aos nossos registros.", + "auth.failed.callback": "Falha ao processar o callback do provedor de login.", + "auth.failed.password": "A senha fornecida está incorreta.", + "auth.failed.email": "Não encontramos nenhum usuário com esse endereço de e-mail.", + "auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.", + "input.name": "Nome", + "input.email": "E-mail", + "input.password": "Senha", + "input.password.again": "Senha novamente", + "input.code": "Código de uso único", + "input.recovery_code": "Código de recuperação", + "button.save": "Salvar", + "repository.url": "Exemplos
Para repositórios públicos, use https://....
Para repositórios privados, use git@....

https://github.com/coollabsio/coolify-examples main branch será selecionado
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify branch será selecionado.
https://gitea.com/sedlav/expressjs.git main branch será selecionado.
https://gitlab.com/andrasbacsai/nodejs-example.git main branch será selecionado.", + "service.stop": "Este serviço será parado.", + "resource.docker_cleanup": "Executar limpeza do Docker (remover imagens não utilizadas e cache de build).", + "resource.non_persistent": "Todos os dados não persistentes serão excluídos.", + "resource.delete_volumes": "Excluir permanentemente todos os volumes associados a este recurso.", + "resource.delete_connected_networks": "Excluir permanentemente todas as redes não predefinidas associadas a este recurso.", + "resource.delete_configurations": "Excluir permanentemente todos os arquivos de configuração do servidor.", + "database.delete_backups_locally": "Todos os backups serão excluídos permanentemente do armazenamento local.", + "warning.sslipdomain": "Sua configuração foi salva, mas o domínio sslip com https NÃO é recomendado, porque os servidores do Let's Encrypt com este domínio público têm limitação de taxa (a validação do certificado SSL falhará).

Use seu próprio domínio em vez disso." +} From 78ce16d50d2c0a37d4afa9189ab963f27ccc38fc Mon Sep 17 00:00:00 2001 From: Alvin Maulana Date: Wed, 2 Apr 2025 20:42:24 +0800 Subject: [PATCH 08/16] feat(lang): add Indonesian language translations (#5513) --- lang/id.json | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lang/id.json diff --git a/lang/id.json b/lang/id.json new file mode 100644 index 000000000..d35556402 --- /dev/null +++ b/lang/id.json @@ -0,0 +1,40 @@ +{ + "auth.login": "Masuk", + "auth.login.authentik": "Masuk dengan Authentik", + "auth.login.azure": "Masuk dengan Microsoft", + "auth.login.bitbucket": "Masuk dengan Bitbucket", + "auth.login.github": "Masuk dengan GitHub", + "auth.login.gitlab": "Masuk dengan Gitlab", + "auth.login.google": "Masuk dengan Google", + "auth.login.infomaniak": "Masuk dengan Infomaniak", + "auth.already_registered": "Sudah terdaftar?", + "auth.confirm_password": "Konfirmasi kata sandi", + "auth.forgot_password": "Lupa kata sandi", + "auth.forgot_password_send_email": "Kirim email reset kata sandi", + "auth.register_now": "Daftar", + "auth.logout": "Keluar", + "auth.register": "Daftar", + "auth.registration_disabled": "Pendaftaran dinonaktifkan. Harap hubungi administrator.", + "auth.reset_password": "Reset kata sandi", + "auth.failed": "Kredensial ini tidak cocok dengan catatan kami.", + "auth.failed.callback": "Gagal memproses callback dari penyedia login.", + "auth.failed.password": "Kata sandi yang diberikan salah.", + "auth.failed.email": "Kami tidak dapat menemukan pengguna dengan alamat e-mail tersebut.", + "auth.throttle": "Terlalu banyak percobaan login. Silakan coba lagi dalam :seconds detik.", + "input.name": "Nama", + "input.email": "Email", + "input.password": "Kata sandi", + "input.password.again": "Kata sandi lagi", + "input.code": "Kode sekali pakai", + "input.recovery_code": "Kode pemulihan", + "button.save": "Simpan", + "repository.url": "Contoh
Untuk repositori Publik, gunakan https://....
Untuk repositori Privat, gunakan git@....

https://github.com/coollabsio/coolify-examples cabang main akan dipilih
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify cabang nodejs-fastify akan dipilih.
https://gitea.com/sedlav/expressjs.git cabang main akan dipilih.
https://gitlab.com/andrasbacsai/nodejs-example.git cabang main akan dipilih.", + "service.stop": "Layanan ini akan dihentikan.", + "resource.docker_cleanup": "Jalankan Pembersihan Docker (hapus gambar yang tidak digunakan dan cache builder).", + "resource.non_persistent": "Semua data non-persisten akan dihapus.", + "resource.delete_volumes": "Hapus permanen semua volume yang terkait dengan sumber daya ini.", + "resource.delete_connected_networks": "Hapus permanen semua jaringan non-predefined yang terkait dengan sumber daya ini.", + "resource.delete_configurations": "Hapus permanen semua file konfigurasi dari server.", + "database.delete_backups_locally": "Semua backup akan dihapus permanen dari penyimpanan lokal.", + "warning.sslipdomain": "Konfigurasi Anda disimpan, tetapi domain sslip dengan https TIDAK direkomendasikan, karena server Let's Encrypt dengan domain publik ini dibatasi (validasi sertifikat SSL akan gagal).

Gunakan domain Anda sendiri sebagai gantinya." +} From 787a56ce51cc99fba5257c261c006e056d1a7be6 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Wed, 2 Apr 2025 14:53:27 +0200 Subject: [PATCH 09/16] fix(service): add health check to Bugsink service (#5512) --- templates/compose/bugsink.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/compose/bugsink.yaml b/templates/compose/bugsink.yaml index bf015d727..5cdca45d1 100644 --- a/templates/compose/bugsink.yaml +++ b/templates/compose/bugsink.yaml @@ -36,6 +36,11 @@ services: depends_on: mysql: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8000/\").raise_for_status()'"] + interval: 5s + timeout: 20s + retries: 10 volumes: - my-datavolume: \ No newline at end of file + my-datavolume: From 78e2d63f3833e371b04b1abb0fa9c82df713371c Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:59:31 +0200 Subject: [PATCH 10/16] chore(service): remove unused code in Bugsink service --- templates/compose/bugsink.yaml | 20 +++++++++----------- templates/service-templates.json | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/templates/compose/bugsink.yaml b/templates/compose/bugsink.yaml index 5cdca45d1..787852ff8 100644 --- a/templates/compose/bugsink.yaml +++ b/templates/compose/bugsink.yaml @@ -3,17 +3,18 @@ # tags: python, error-tracking, django, mysql # logo: svgs/bugsink.svg # port: 8000 + services: mysql: image: 'mysql:latest' restart: unless-stopped environment: - - 'MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT}' - - 'MYSQL_DATABASE=${MYSQL_DATABASE:-bugsink}' - - 'MYSQL_USER=${SERVICE_USER_BUGSINK}' - - 'MYSQL_PASSWORD=${SERVICE_PASSWORD_BUGSINK}' + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT} + - MYSQL_DATABASE=${MYSQL_DATABASE:-bugsink} + - MYSQL_USER=${SERVICE_USER_BUGSINK} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_BUGSINK} volumes: - - 'my-datavolume:/var/lib/mysql' + - my-datavolume:/var/lib/mysql healthcheck: test: - CMD @@ -29,10 +30,10 @@ services: restart: unless-stopped environment: - SECRET_KEY=$SERVICE_PASSWORD_64_BUGSINK - - 'CREATE_SUPERUSER=admin:${SERVICE_PASSWORD_BUGSINK}' + - CREATE_SUPERUSER=admin:$SERVICE_PASSWORD_BUGSINK - SERVICE_FQDN_BUGSINK_8000 - - 'BASE_URL=${SERVICE_FQDN_BUGSINK_8000}' - - 'DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink}' + - BASE_URL=$SERVICE_FQDN_BUGSINK_8000 + - DATABASE_URL=mysql://${SERVICE_USER_BUGSINK}:$SERVICE_PASSWORD_BUGSINK@mysql:3306/${MYSQL_DATABASE:-bugsink} depends_on: mysql: condition: service_healthy @@ -41,6 +42,3 @@ services: interval: 5s timeout: 20s retries: 10 - -volumes: - my-datavolume: diff --git a/templates/service-templates.json b/templates/service-templates.json index e300aaf39..531bb639a 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -271,7 +271,7 @@ "bugsink": { "documentation": "https://www.bugsink.com/docs/?utm_source=coolify.io", "slogan": "Self-hosted Error Tracking", - "compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiR7U0VSVklDRV9QQVNTV09SRF9CVUdTSU5LfScKICAgICAgLSBTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0JBU0VfVVJMPSR7U0VSVklDRV9GUUROX0JVR1NJTktfODAwMH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CnZvbHVtZXM6CiAgbXktZGF0YXZvbHVtZTogbnVsbAo=", + "compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOmxhdGVzdCcKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJ1Z3Npbmt9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX0JVR1NJTkt9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQlVHU0lOS30nCiAgICB2b2x1bWVzOgogICAgICAtICdteS1kYXRhdm9sdW1lOi92YXIvbGliL215c3FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGJ1Z3NpbmsvYnVnc2luawogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQlVHU0lOSwogICAgICAtICdDUkVBVEVfU1VQRVJVU0VSPWFkbWluOiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTksnCiAgICAgIC0gU0VSVklDRV9GUUROX0JVR1NJTktfODAwMAogICAgICAtIEJBU0VfVVJMPSRTRVJWSUNFX0ZRRE5fQlVHU0lOS184MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX0JVR1NJTkt9OiRTRVJWSUNFX1BBU1NXT1JEX0JVR1NJTktAbXlzcWw6MzMwNi8ke01ZU1FMX0RBVEFCQVNFOi1idWdzaW5rfScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3B5dGhvbiAtYyAnJ2ltcG9ydCByZXF1ZXN0czsgcmVxdWVzdHMuZ2V0KCJodHRwOi8vbG9jYWxob3N0OjgwMDAvIikucmFpc2VfZm9yX3N0YXR1cygpJycnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "python", "error-tracking", From d15f1b9b94a2082c48dcdf7a4d3c5bab83640bf0 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 2 Apr 2025 16:28:14 +0200 Subject: [PATCH 11/16] fix(email): Emails are not sent in multiple cases - fix(email): transactional emails are all not sent if `Use system wide (transactional) email settings` is disabled and no other email provide is setup on the Notifications page - fix(email): no emails are sent if SMTP username and SMTP password are empty (which is the case in dev for example) - fix(email): Wrong test email notification is used, causing the transactional email test notification to fail if no email provider is set up on the Notifications page. --- app/Livewire/SettingsEmail.php | 2 +- app/Notifications/Channels/EmailChannel.php | 7 ++++--- app/Notifications/TransactionalEmails/InvitationLink.php | 3 ++- app/Notifications/TransactionalEmails/ResetPassword.php | 3 ++- app/Notifications/TransactionalEmails/Test.php | 3 ++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 858f3b187..4426c7812 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -4,7 +4,7 @@ namespace App\Livewire; use App\Models\InstanceSettings; use App\Models\Team; -use App\Notifications\Test; +use App\Notifications\TransactionalEmails\Test; use Illuminate\Support\Facades\RateLimiter; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index a452ef8dd..8a9a95107 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -12,8 +12,9 @@ class EmailChannel public function send(SendsEmail $notifiable, Notification $notification): void { $useInstanceEmailSettings = $notifiable->emailNotificationSettings->use_instance_email_settings; + $isTransactionalEmail = data_get($notification, 'isTransactionalEmail', false); $customEmails = data_get($notification, 'emails', null); - if ($useInstanceEmailSettings) { + if ($useInstanceEmailSettings || $isTransactionalEmail) { $settings = instanceSettings(); } else { $settings = $notifiable->emailNotificationSettings; @@ -49,8 +50,8 @@ class EmailChannel $settings->smtp_port, $encryption ); - $transport->setUsername($settings->smtp_username); - $transport->setPassword($settings->smtp_password); + $transport->setUsername($settings->smtp_username ?? ''); + $transport->setPassword($settings->smtp_password ?? ''); $mailer = new \Symfony\Component\Mailer\Mailer($transport); diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php index 30ace99dc..fce3eb948 100644 --- a/app/Notifications/TransactionalEmails/InvitationLink.php +++ b/app/Notifications/TransactionalEmails/InvitationLink.php @@ -16,9 +16,10 @@ class InvitationLink extends CustomEmailNotification return [TransactionalEmailChannel::class]; } - public function __construct(public User $user) + public function __construct(public User $user, public bool $isTransactionalEmail) { $this->onQueue('high'); + $this->isTransactionalEmail = true; } public function toMail(): MailMessage diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 4593ddb0d..031a2d95a 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -17,10 +17,11 @@ class ResetPassword extends Notification public InstanceSettings $settings; - public function __construct($token) + public function __construct($token, public bool $isTransactionalEmail) { $this->settings = instanceSettings(); $this->token = $token; + $this->isTransactionalEmail = true; } public static function createUrlUsing($callback) diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index eeb32a254..951c007d9 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -8,9 +8,10 @@ use Illuminate\Notifications\Messages\MailMessage; class Test extends CustomEmailNotification { - public function __construct(public string $emails) + public function __construct(public string $emails, public string $isTransactionalEmail) { $this->onQueue('high'); + $this->isTransactionalEmail = true; } public function via(): array From 9f58128623bc1a969fedeb3f1c3b77dcb434349c Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 2 Apr 2025 17:03:13 +0200 Subject: [PATCH 12/16] fix(deployments): use graceful shutdown instead of `rm` --- app/Jobs/ApplicationDeploymentJob.php | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index b72d2ade3..5dbdbf215 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -329,13 +329,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->write_deployment_configurations(); } - $this->execute_remote_command( - [ - "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1", - 'hidden' => true, - 'ignore_errors' => true, - ] - ); + $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}"); + $this->graceful_shutdown_container($this->deployment_uuid); ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } @@ -1366,13 +1361,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } $this->application_deployment_queue->addLogEntry("Preparing container with helper image: $helperImage."); - $this->execute_remote_command( - [ - 'command' => "docker rm -f {$this->deployment_uuid}", - 'ignore_errors' => true, - 'hidden' => true, - ] - ); + $this->application_deployment_queue->addLogEntry("Starting graceful shutdown container: {$this->deployment_uuid}"); + $this->graceful_shutdown_container($this->deployment_uuid); $this->execute_remote_command( [ $runCommand, From 2248028a84cbd15a745a079fbfd90000fcb8e6b8 Mon Sep 17 00:00:00 2001 From: Will Russell Date: Wed, 2 Apr 2025 18:18:13 +0100 Subject: [PATCH 13/16] fix(docs): contribute service url (#5517) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b642fdf5..1ba4d1876 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -238,7 +238,7 @@ After completing these steps, you'll have a fresh development setup. ### Contributing a New Service To add a new service to Coolify, please refer to our documentation: -[Adding a New Service](https://coolify.io/docs/knowledge-base/contribute/service) +[Adding a New Service](https://coolify.io/docs/get-started/contribute/service) ### Contributing to Documentation From 331693546bfb7197ac773062d12c88e78e6f9474 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 2 Apr 2025 19:38:31 +0200 Subject: [PATCH 14/16] fix(proxy): proxy restart does not work on domain - When you restart the proxy on an instance domain, the proxy stops and is removed, but never restarted. So you loose access over the domain and have to go in over IP and Port. This is because we are doing the restart directly in the UI instead of in the background via a job, and the proxy is serving the UI domain. --- app/Actions/Proxy/StopProxy.php | 56 ++++++++++++++++++++++++++++ app/Jobs/RestartProxyJob.php | 46 +++++++++++++++++++++++ app/Livewire/Server/Proxy/Deploy.php | 43 +++------------------ 3 files changed, 107 insertions(+), 38 deletions(-) create mode 100644 app/Actions/Proxy/StopProxy.php create mode 100644 app/Jobs/RestartProxyJob.php diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php new file mode 100644 index 000000000..a5dcc6cf4 --- /dev/null +++ b/app/Actions/Proxy/StopProxy.php @@ -0,0 +1,56 @@ +isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; + $timeout = 30; + + $process = $this->stopContainer($containerName, $timeout); + + $startTime = Carbon::now()->getTimestamp(); + while ($process->running()) { + if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { + $this->forceStopContainer($containerName, $server); + break; + } + usleep(100000); + } + + $this->removeContainer($containerName, $server); + } catch (\Throwable $e) { + return handleError($e); + } finally { + $server->proxy->force_stop = $forceStop; + $server->proxy->status = 'exited'; + $server->save(); + } + } + + private function stopContainer(string $containerName, int $timeout): InvokedProcess + { + return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); + } + + private function forceStopContainer(string $containerName, Server $server) + { + instant_remote_process(["docker kill $containerName"], $server, throwError: false); + } + + private function removeContainer(string $containerName, Server $server) + { + instant_remote_process(["docker rm -f $containerName"], $server, throwError: false); + } +} diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php new file mode 100644 index 000000000..7fc716f70 --- /dev/null +++ b/app/Jobs/RestartProxyJob.php @@ -0,0 +1,46 @@ +server->uuid))->dontRelease()]; + } + + public function __construct(public Server $server) {} + + public function handle() + { + try { + StopProxy::run($this->server); + + $this->server->proxy->force_stop = false; + $this->server->save(); + StartProxy::run($this->server, force: true); + + CheckProxy::run($this->server, true); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index 4a7e4124e..48eede4e5 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -4,11 +4,10 @@ namespace App\Livewire\Server\Proxy; use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; +use App\Actions\Proxy\StopProxy; use App\Events\ProxyStatusChanged; +use App\Jobs\RestartProxyJob; use App\Models\Server; -use Carbon\Carbon; -use Illuminate\Process\InvokedProcess; -use Illuminate\Support\Facades\Process; use Livewire\Component; class Deploy extends Component @@ -65,6 +64,7 @@ class Deploy extends Component public function restart() { try { + RestartProxyJob::dispatch($this->server); $this->dispatch('checkProxy'); } catch (\Throwable $e) { return handleError($e, $this); @@ -97,43 +97,10 @@ class Deploy extends Component public function stop(bool $forceStop = true) { try { - $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; - $timeout = 30; - - $process = $this->stopContainer($containerName, $timeout); - - $startTime = Carbon::now()->getTimestamp(); - while ($process->running()) { - if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { - $this->forceStopContainer($containerName); - break; - } - usleep(100000); - } - - $this->removeContainer($containerName); + StopProxy::run($this->server, $forceStop); + $this->dispatch('proxyStatusUpdated'); } catch (\Throwable $e) { return handleError($e, $this); - } finally { - $this->server->proxy->force_stop = $forceStop; - $this->server->proxy->status = 'exited'; - $this->server->save(); - $this->dispatch('proxyStatusUpdated'); } } - - private function stopContainer(string $containerName, int $timeout): InvokedProcess - { - return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); - } - - private function forceStopContainer(string $containerName) - { - instant_remote_process(["docker kill $containerName"], $this->server, throwError: false); - } - - private function removeContainer(string $containerName) - { - instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false); - } } From 7e0ff9cc3cf60bbb1228446692ae09df2cfe0e21 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Wed, 2 Apr 2025 19:38:53 +0200 Subject: [PATCH 15/16] fix(ui): only show copy button on https --- resources/views/components/forms/copy-button.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/components/forms/copy-button.blade.php b/resources/views/components/forms/copy-button.blade.php index 39be2bc6a..12fadc595 100644 --- a/resources/views/components/forms/copy-button.blade.php +++ b/resources/views/components/forms/copy-button.blade.php @@ -1,8 +1,9 @@ @props(['text']) -
+