From 3b3090650f8df6fe8f1257133dc3214e46f28996 Mon Sep 17 00:00:00 2001 From: flicko <77581181+flickowoa@users.noreply.github.com> Date: Fri, 4 Jul 2025 15:06:49 +0530 Subject: [PATCH 01/18] support cachyos --- scripts/install.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/install.sh b/scripts/install.sh index ae237a54a..b7a066616 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -253,6 +253,11 @@ if [ "$OS_TYPE" = "endeavouros" ]; then OS_TYPE="arch" fi +# Check if the OS is Cachy OS, if so, change it to arch +if [ "$OS_TYPE" = "cachyos" ]; then + OS_TYPE="arch" +fi + # Check if the OS is Asahi Linux, if so, change it to fedora if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then OS_TYPE="fedora" From 2733aeb092842a5d30f21375549307f513ea6cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Rodr=C3=ADguez?= Date: Fri, 4 Jul 2025 15:16:01 +0200 Subject: [PATCH 02/18] fix(install.sh): use IPV4_PUBLIC_IP variable in output instead of repeated curl --- scripts/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install.sh b/scripts/install.sh index ae237a54a..67d995566 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -844,7 +844,7 @@ IPV6_PUBLIC_IP=$(curl -6s https://ifconfig.io || true) echo -e "\nYour instance is ready to use!\n" if [ -n "$IPV4_PUBLIC_IP" ]; then - echo -e "You can access Coolify through your Public IPV4: http://$(curl -4s https://ifconfig.io):8000" + echo -e "You can access Coolify through your Public IPV4: http://[$IPV4_PUBLIC_IP]:8000" fi if [ -n "$IPV6_PUBLIC_IP" ]; then echo -e "You can access Coolify through your Public IPv6: http://[$IPV6_PUBLIC_IP]:8000" From 4c4b115008fa202a6832d5f87dccc4095be8934a Mon Sep 17 00:00:00 2001 From: Nathan James <64075030+Nathanjms@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:06:53 +0100 Subject: [PATCH 03/18] Correct Typo: 'form' -> 'from' --- app/Livewire/Project/Database/BackupExecutions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index 3fc721fda..f96ca9a6a 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -121,8 +121,8 @@ class BackupExecutions extends Component { return view('livewire.project.database.backup-executions', [ 'checkboxes' => [ - ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'], - // ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'], + ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently from S3 Storage'], + // ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently from SFTP Storage'], ], ]); } From 70db5be7c775d347460f29c0f3a0c03e6e00288c Mon Sep 17 00:00:00 2001 From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com> Date: Mon, 7 Jul 2025 05:21:49 +0530 Subject: [PATCH 04/18] Set Service Postiz to v1.60.1 --- templates/compose/postiz.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/postiz.yaml b/templates/compose/postiz.yaml index 6060fb8a6..2631e16fe 100644 --- a/templates/compose/postiz.yaml +++ b/templates/compose/postiz.yaml @@ -6,7 +6,7 @@ services: postiz: - image: ghcr.io/gitroomhq/postiz-app:latest + image: ghcr.io/gitroomhq/postiz-app:v1.60.1 environment: - SERVICE_FQDN_POSTIZ_5000 - MAIN_URL=${SERVICE_FQDN_POSTIZ} From 7817c9cad759d7fc670f3ea639a9b28c7840798c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 09:47:11 +0200 Subject: [PATCH 05/18] refactor(redis): enhance CleanupRedis command with dry-run option and improved key deletion logic --- app/Console/Commands/CleanupRedis.php | 258 +++++++++++++++++++++++++- 1 file changed, 251 insertions(+), 7 deletions(-) diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php index 315d1adc7..a13cda0b8 100644 --- a/app/Console/Commands/CleanupRedis.php +++ b/app/Console/Commands/CleanupRedis.php @@ -7,26 +7,270 @@ use Illuminate\Support\Facades\Redis; class CleanupRedis extends Command { - protected $signature = 'cleanup:redis'; + protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup}'; - protected $description = 'Cleanup Redis'; + protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, and related data)'; public function handle() { $redis = Redis::connection('horizon'); - $keys = $redis->keys('*'); $prefix = config('horizon.prefix'); + $dryRun = $this->option('dry-run'); + $skipOverlapping = $this->option('skip-overlapping'); + + if ($dryRun) { + $this->info('DRY RUN MODE - No data will be deleted'); + } + + $deletedCount = 0; + $totalKeys = 0; + + // Get all keys with the horizon prefix + $keys = $redis->keys('*'); + $totalKeys = count($keys); + + $this->info("Scanning {$totalKeys} keys for cleanup..."); + foreach ($keys as $key) { $keyWithoutPrefix = str_replace($prefix, '', $key); $type = $redis->command('type', [$keyWithoutPrefix]); + // Handle hash-type keys (individual jobs) if ($type === 5) { - $data = $redis->command('hgetall', [$keyWithoutPrefix]); - $status = data_get($data, 'status'); - if ($status === 'completed') { - $redis->command('del', [$keyWithoutPrefix]); + if ($this->shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun)) { + $deletedCount++; + } + } + // Handle other key types (metrics, lists, etc.) + else { + if ($this->shouldDeleteOtherKey($redis, $keyWithoutPrefix, $key, $dryRun)) { + $deletedCount++; } } } + + // Clean up overlapping queues if not skipped + if (! $skipOverlapping) { + $this->info('Cleaning up overlapping queues...'); + $overlappingCleaned = $this->cleanupOverlappingQueues($redis, $prefix, $dryRun); + $deletedCount += $overlappingCleaned; + } + + if ($dryRun) { + $this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys"); + } else { + $this->info("Deleted {$deletedCount} out of {$totalKeys} keys"); + } + } + + private function shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun) + { + $data = $redis->command('hgetall', [$keyWithoutPrefix]); + $status = data_get($data, 'status'); + + // Delete completed and failed jobs + if (in_array($status, ['completed', 'failed'])) { + if ($dryRun) { + $this->line("Would delete job: {$keyWithoutPrefix} (status: {$status})"); + } else { + $redis->command('del', [$keyWithoutPrefix]); + $this->line("Deleted job: {$keyWithoutPrefix} (status: {$status})"); + } + + return true; + } + + return false; + } + + private function shouldDeleteOtherKey($redis, $keyWithoutPrefix, $fullKey, $dryRun) + { + // Clean up various Horizon data structures + $patterns = [ + 'recent_jobs' => 'Recent jobs list', + 'failed_jobs' => 'Failed jobs list', + 'completed_jobs' => 'Completed jobs list', + 'job_classes' => 'Job classes metrics', + 'queues' => 'Queue metrics', + 'processes' => 'Process metrics', + 'supervisors' => 'Supervisor data', + 'metrics' => 'General metrics', + 'workload' => 'Workload data', + ]; + + foreach ($patterns as $pattern => $description) { + if (str_contains($keyWithoutPrefix, $pattern)) { + if ($dryRun) { + $this->line("Would delete {$description}: {$keyWithoutPrefix}"); + } else { + $redis->command('del', [$keyWithoutPrefix]); + $this->line("Deleted {$description}: {$keyWithoutPrefix}"); + } + + return true; + } + } + + // Clean up old timestamped data (older than 7 days) + if (preg_match('/(\d{10})/', $keyWithoutPrefix, $matches)) { + $timestamp = (int) $matches[1]; + $weekAgo = now()->subDays(7)->timestamp; + + if ($timestamp < $weekAgo) { + if ($dryRun) { + $this->line("Would delete old timestamped data: {$keyWithoutPrefix}"); + } else { + $redis->command('del', [$keyWithoutPrefix]); + $this->line("Deleted old timestamped data: {$keyWithoutPrefix}"); + } + + return true; + } + } + + return false; + } + + private function cleanupOverlappingQueues($redis, $prefix, $dryRun) + { + $cleanedCount = 0; + $queueKeys = []; + + // Find all queue-related keys + $allKeys = $redis->keys('*'); + foreach ($allKeys as $key) { + $keyWithoutPrefix = str_replace($prefix, '', $key); + if (str_contains($keyWithoutPrefix, 'queue:') || preg_match('/queues?[:\-]/', $keyWithoutPrefix)) { + $queueKeys[] = $keyWithoutPrefix; + } + } + + $this->info('Found '.count($queueKeys).' queue-related keys'); + + // Group queues by name pattern to find duplicates + $queueGroups = []; + foreach ($queueKeys as $queueKey) { + // Extract queue name (remove timestamps, suffixes) + $baseName = preg_replace('/[:\-]\d+$/', '', $queueKey); + $baseName = preg_replace('/[:\-](pending|reserved|delayed|processing)$/', '', $baseName); + + if (! isset($queueGroups[$baseName])) { + $queueGroups[$baseName] = []; + } + $queueGroups[$baseName][] = $queueKey; + } + + // Process each group for overlaps + foreach ($queueGroups as $baseName => $keys) { + if (count($keys) > 1) { + $cleanedCount += $this->deduplicateQueueGroup($redis, $baseName, $keys, $dryRun); + } + + // Also check for duplicate jobs within individual queues + foreach ($keys as $queueKey) { + $cleanedCount += $this->deduplicateQueueContents($redis, $queueKey, $dryRun); + } + } + + return $cleanedCount; + } + + private function deduplicateQueueGroup($redis, $baseName, $keys, $dryRun) + { + $cleanedCount = 0; + $this->line("Processing queue group: {$baseName} (".count($keys).' keys)'); + + // Sort keys to keep the most recent one + usort($keys, function ($a, $b) { + // Prefer keys without timestamps (they're usually the main queue) + $aHasTimestamp = preg_match('/\d{10}/', $a); + $bHasTimestamp = preg_match('/\d{10}/', $b); + + if ($aHasTimestamp && ! $bHasTimestamp) { + return 1; + } + if (! $aHasTimestamp && $bHasTimestamp) { + return -1; + } + + // If both have timestamps, prefer the newer one + if ($aHasTimestamp && $bHasTimestamp) { + preg_match('/(\d{10})/', $a, $aMatches); + preg_match('/(\d{10})/', $b, $bMatches); + + return ($bMatches[1] ?? 0) <=> ($aMatches[1] ?? 0); + } + + return strcmp($a, $b); + }); + + // Keep the first (preferred) key, remove others that are empty or redundant + $keepKey = array_shift($keys); + + foreach ($keys as $redundantKey) { + $type = $redis->command('type', [$redundantKey]); + $shouldDelete = false; + + if ($type === 1) { // LIST type + $length = $redis->command('llen', [$redundantKey]); + if ($length == 0) { + $shouldDelete = true; + } + } elseif ($type === 3) { // SET type + $count = $redis->command('scard', [$redundantKey]); + if ($count == 0) { + $shouldDelete = true; + } + } elseif ($type === 4) { // ZSET type + $count = $redis->command('zcard', [$redundantKey]); + if ($count == 0) { + $shouldDelete = true; + } + } + + if ($shouldDelete) { + if ($dryRun) { + $this->line(" Would delete empty queue: {$redundantKey}"); + } else { + $redis->command('del', [$redundantKey]); + $this->line(" Deleted empty queue: {$redundantKey}"); + } + $cleanedCount++; + } + } + + return $cleanedCount; + } + + private function deduplicateQueueContents($redis, $queueKey, $dryRun) + { + $cleanedCount = 0; + $type = $redis->command('type', [$queueKey]); + + if ($type === 1) { // LIST type - common for job queues + $length = $redis->command('llen', [$queueKey]); + if ($length > 1) { + $items = $redis->command('lrange', [$queueKey, 0, -1]); + $uniqueItems = array_unique($items); + + if (count($uniqueItems) < count($items)) { + $duplicates = count($items) - count($uniqueItems); + + if ($dryRun) { + $this->line(" Would remove {$duplicates} duplicate jobs from queue: {$queueKey}"); + } else { + // Rebuild the list with unique items + $redis->command('del', [$queueKey]); + foreach (array_reverse($uniqueItems) as $item) { + $redis->command('lpush', [$queueKey, $item]); + } + $this->line(" Removed {$duplicates} duplicate jobs from queue: {$queueKey}"); + } + $cleanedCount += $duplicates; + } + } + } + + return $cleanedCount; } } From 3b7f4bcbbd21f11e39a3d985c01214d41caed81c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 09:50:15 +0200 Subject: [PATCH 06/18] refactor(init): standardize method naming conventions and improve command structure in Init.php --- app/Console/Commands/Init.php | 78 +++++++++++++++++------------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index a954b10fd..1a7c0911f 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -36,24 +36,20 @@ class Init extends Command $this->servers = Server::all(); if (! isCloud()) { - $this->send_alive_signal(); + $this->sendAliveSignal(); get_public_ips(); } // Backward compatibility - $this->replace_slash_in_environment_name(); - $this->restore_coolify_db_backup(); - $this->update_user_emails(); + $this->replaceSlashInEnvironmentName(); + $this->restoreCoolifyDbBackup(); + $this->updateUserEmails(); // - $this->update_traefik_labels(); + $this->updateTraefikLabels(); if (! isCloud() || $this->option('force-cloud')) { - $this->cleanup_unused_network_from_coolify_proxy(); - } - if (isCloud()) { - $this->cleanup_unnecessary_dynamic_proxy_configuration(); - } else { - $this->cleanup_in_progress_application_deployments(); + $this->cleanupUnusedNetworkFromCoolifyProxy(); } + $this->call('cleanup:redis'); $this->call('cleanup:stucked-resources'); @@ -66,33 +62,35 @@ class Init extends Command if (isCloud()) { try { + $this->cleanupUnnecessaryDynamicProxyConfiguration(); $this->pullTemplatesFromCDN(); } catch (\Throwable $e) { echo "Could not pull templates from CDN: {$e->getMessage()}\n"; } + + return; } - if (! isCloud()) { - try { - $this->pullTemplatesFromCDN(); - } catch (\Throwable $e) { - echo "Could not pull templates from CDN: {$e->getMessage()}\n"; - } - try { - $localhost = $this->servers->where('id', 0)->first(); - $localhost->setupDynamicProxyConfiguration(); - } catch (\Throwable $e) { - echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; - } - $settings = instanceSettings(); - if (! is_null(config('constants.coolify.autoupdate', null))) { - if (config('constants.coolify.autoupdate') == true) { - echo "Enabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => true]); - } else { - echo "Disabling auto-update\n"; - $settings->update(['is_auto_update_enabled' => false]); - } + try { + $this->cleanupInProgressApplicationDeployments(); + $this->pullTemplatesFromCDN(); + } catch (\Throwable $e) { + echo "Could not pull templates from CDN: {$e->getMessage()}\n"; + } + try { + $localhost = $this->servers->where('id', 0)->first(); + $localhost->setupDynamicProxyConfiguration(); + } catch (\Throwable $e) { + echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; + } + $settings = instanceSettings(); + if (! is_null(config('constants.coolify.autoupdate', null))) { + if (config('constants.coolify.autoupdate') == true) { + echo "Enabling auto-update\n"; + $settings->update(['is_auto_update_enabled' => true]); + } else { + echo "Disabling auto-update\n"; + $settings->update(['is_auto_update_enabled' => false]); } } } @@ -117,7 +115,7 @@ class Init extends Command Artisan::call('optimize'); } - private function update_user_emails() + private function updateUserEmails() { try { User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) { @@ -128,7 +126,7 @@ class Init extends Command } } - private function update_traefik_labels() + private function updateTraefikLabels() { try { Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']); @@ -137,7 +135,7 @@ class Init extends Command } } - private function cleanup_unnecessary_dynamic_proxy_configuration() + private function cleanupUnnecessaryDynamicProxyConfiguration() { foreach ($this->servers as $server) { try { @@ -158,7 +156,7 @@ class Init extends Command } } - private function cleanup_unused_network_from_coolify_proxy() + private function cleanupUnusedNetworkFromCoolifyProxy() { foreach ($this->servers as $server) { if (! $server->isFunctional()) { @@ -197,7 +195,7 @@ class Init extends Command } } - private function restore_coolify_db_backup() + private function restoreCoolifyDbBackup() { if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) { try { @@ -223,7 +221,7 @@ class Init extends Command } } - private function send_alive_signal() + private function sendAliveSignal() { $id = config('app.id'); $version = config('constants.coolify.version'); @@ -241,7 +239,7 @@ class Init extends Command } } - private function cleanup_in_progress_application_deployments() + private function cleanupInProgressApplicationDeployments() { // Cleanup any failed deployments try { @@ -258,7 +256,7 @@ class Init extends Command } } - private function replace_slash_in_environment_name() + private function replaceSlashInEnvironmentName() { if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) { $environments = Environment::all(); From f732220b8f5c2e52753a6aac37f94299b6a5bf30 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:20:54 +0200 Subject: [PATCH 07/18] refactor(shared): improve error handling in getTopLevelNetworks function to return network name on invalid docker-compose.yml --- bootstrap/helpers/shared.php | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 9e1aa0a43..3685dcfd5 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -599,7 +599,15 @@ function getTopLevelNetworks(Service|Application $resource) try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { - throw new \RuntimeException($e->getMessage()); + // If the docker-compose.yml file is not valid, we will return the network name as the key + $topLevelNetworks = collect([ + $resource->uuid => [ + 'name' => $resource->uuid, + 'external' => true, + ], + ]); + + return $topLevelNetworks->keys(); } $services = data_get($yaml, 'services'); $topLevelNetworks = collect(data_get($yaml, 'networks', [])); @@ -653,9 +661,16 @@ function getTopLevelNetworks(Service|Application $resource) try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { - throw new \RuntimeException($e->getMessage()); + // If the docker-compose.yml file is not valid, we will return the network name as the key + $topLevelNetworks = collect([ + $resource->uuid => [ + 'name' => $resource->uuid, + 'external' => true, + ], + ]); + + return $topLevelNetworks->keys(); } - $server = $resource->destination->server; $topLevelNetworks = collect(data_get($yaml, 'networks', [])); $services = data_get($yaml, 'services'); $definedNetwork = collect([$resource->uuid]); From 9b8da285c0977bc90bb8ed24b937b51a91c1d5ff Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:23:31 +0200 Subject: [PATCH 08/18] refactor(database): improve error handling for unsupported database types in StartDatabaseProxy --- app/Actions/Database/StartDatabaseProxy.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 21fd6eb97..5c2f4d7fd 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -47,7 +47,6 @@ class StartDatabaseProxy if ($isSSLEnabled) { $internalPort = match ($databaseType) { 'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6380, - default => throw new \Exception("Unsupported database type: $databaseType"), }; } From 13412262d213d0f19d8136bd0fe3dd34fefa5079 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:26:45 +0200 Subject: [PATCH 09/18] chore(versions): update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively --- config/constants.php | 2 +- versions.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/constants.php b/config/constants.php index 67e24d80d..8fbc3a6cb 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.420.3', + 'version' => '4.0.0-beta.420.4', 'helper_version' => '1.0.8', 'realtime_version' => '1.0.9', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/versions.json b/versions.json index d4959cdfd..f93f0350c 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.420.3" + "version": "4.0.0-beta.420.4" }, "nightly": { - "version": "4.0.0-beta.420.4" + "version": "4.0.0-beta.420.5" }, "helper": { "version": "1.0.8" From ec0a17998eec82ae54f3e011161e0ec9ebdbe215 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:30:38 +0200 Subject: [PATCH 10/18] fix(service): update Postiz compose configuration for improved server availability --- templates/service-templates.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/service-templates.json b/templates/service-templates.json index 4302f4d21..d308acb7c 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -2798,7 +2798,7 @@ "postiz": { "documentation": "https://docs.postiz.com?utm_source=coolify.io", "slogan": "Open source social media scheduling tool.", - "compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9QT1NUSVpfNTAwMAogICAgICAtICdNQUlOX1VSTD0ke1NFUlZJQ0VfRlFETl9QT1NUSVp9JwogICAgICAtICdGUk9OVEVORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafS9hcGknCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVFNFQ1JFVH0nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vcG9zdGdyZXM6JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9QHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1wb3N0aXotZGJ9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OScKICAgICAgLSAnQkFDS0VORF9JTlRFUk5BTF9VUkw9aHR0cDovL2xvY2FsaG9zdDozMDAwJwogICAgICAtICdDTE9VREZMQVJFX0FDQ09VTlRfSUQ9JHtDTE9VREZMQVJFX0FDQ09VTlRfSUR9JwogICAgICAtICdDTE9VREZMQVJFX0FDQ0VTU19LRVk9JHtDTE9VREZMQVJFX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX1NFQ1JFVF9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUTkFNRT0ke0NMT1VERkxBUkVfQlVDS0VUTkFNRX0nCiAgICAgIC0gJ0NMT1VERkxBUkVfQlVDS0VUX1VSTD0ke0NMT1VERkxBUkVfQlVDS0VUX1VSTH0nCiAgICAgIC0gJ0NMT1VERkxBUkVfUkVHSU9OPSR7Q0xPVURGTEFSRV9SRUdJT059JwogICAgICAtICdTVE9SQUdFX1BST1ZJREVSPSR7U1RPUkFHRV9QUk9WSURFUjotbG9jYWx9JwogICAgICAtICdVUExPQURfRElSRUNUT1JZPSR7VVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfRElSRUNUT1JZPSR7TkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWTotL3VwbG9hZHN9JwogICAgICAtICdORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9TVEFUSUNfRElSRUNUT1JZfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fQUREUkVTUz0ke0VNQUlMX0ZST01fQUREUkVTU30nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ0VNQUlMX1BST1ZJREVSPSR7RU1BSUxfUFJPVklERVJ9JwogICAgICAtICdYX0FQSV9LRVk9JHtTRVJWSUNFX1hfQVBJfScKICAgICAgLSAnWF9BUElfU0VDUkVUPSR7U0VSVklDRV9YX1NFQ1JFVH0nCiAgICAgIC0gJ0xJTktFRElOX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTElOS0VESU5fSUR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9MSU5LRURJTl9TRUNSRVR9JwogICAgICAtICdSRURESVRfQ0xJRU5UX0lEPSR7U0VSVklDRV9SRURESVRfQVBJfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1JFRERJVF9TRUNSRVR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX0lEPSR7U0VSVklDRV9HSVRIVUJfSUR9JwogICAgICAtICdHSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfR0lUSFVCX1NFQ1JFVH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX0lEPSR7U0VSVklDRV9USFJFQURTX0lEfScKICAgICAgLSAnVEhSRUFEU19BUFBfU0VDUkVUPSR7U0VSVklDRV9USFJFQURTX1NFQ1JFVH0nCiAgICAgIC0gJ0ZBQ0VCT09LX0FQUF9JRD0ke1NFUlZJQ0VfRkFDRUJPT0tfSUR9JwogICAgICAtICdGQUNFQk9PS19BUFBfU0VDUkVUPSR7U0VSVklDRV9GQUNFQk9PS19TRUNSRVR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfWU9VVFVCRV9JRH0nCiAgICAgIC0gJ1lPVVRVQkVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfWU9VVFVCRV9TRUNSRVR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX0lEPSR7U0VSVklDRV9USUtUT0tfSUR9JwogICAgICAtICdUSUtUT0tfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfVElLVE9LX1NFQ1JFVH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfSUQ9JHtTRVJWSUNFX1BJTlRFUkVTVF9JRH0nCiAgICAgIC0gJ1BJTlRFUkVTVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9QSU5URVJFU1RfU0VDUkVUfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX0lEPSR7U0VSVklDRV9EUklCQkxFX0lEfScKICAgICAgLSAnRFJJQkJCTEVfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRFJJQkJMRV9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRElTQ09SRF9JRH0nCiAgICAgIC0gJ0RJU0NPUkRfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfRElTQ09SRF9TRUNSRVR9JwogICAgICAtICdESVNDT1JEX0JPVF9UT0tFTl9JRD0ke1NFUlZJQ0VfRElTQ09SRF9UT0tFTn0nCiAgICAgIC0gJ1NMQUNLX0lEPSR7U0VSVklDRV9TTEFDS19JRH0nCiAgICAgIC0gJ1NMQUNLX1NFQ1JFVD0ke1NFUlZJQ0VfU0xBQ0tfU0VDUkVUfScKICAgICAgLSAnU0xBQ0tfU0lHTklOR19TRUNSRVQ9JHtTTEFDS19TSUdOSU5HX1NFQ1JFVH0nCiAgICAgIC0gJ01BU1RPRE9OX0NMSUVOVF9JRD0ke1NFUlZJQ0VfTUFTVE9ET05fSUR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9NQVNUT0RPTl9TRUNSRVR9JwogICAgICAtICdCRUVISUlWRV9BUElfS0VZPSR7U0VSVklDRV9CRUVISUlWRV9LRVl9JwogICAgICAtICdCRUVISUlWRV9QVUJMSUNBVElPTl9JRD0ke1NFUlZJQ0VfQkVFSElJVkVfUFVCSUR9JwogICAgICAtICdPUEVOQUlfQVBJX0tFWT0ke1NFUlZJQ0VfT1BFTkFJX0tFWX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVD0ke05FWFRfUFVCTElDX0RJU0NPUkRfU1VQUE9SVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX1BPTE9UTk89JHtORVhUX1BVQkxJQ19QT0xPVE5PfScKICAgICAgLSBJU19HRU5FUkFMPXRydWUKICAgICAgLSAnTlhfQUREX1BMVUdJTlM9JHtOWF9BRERfUExVR0lOUzotZmFsc2V9JwogICAgICAtICdGRUVfQU1PVU5UPSR7RkVFX0FNT1VOVDotMC4wNX0nCiAgICAgIC0gJ1NUUklQRV9QVUJMSVNIQUJMRV9LRVk9JHtTVFJJUEVfUFVCTElTSEFCTEVfS0VZfScKICAgICAgLSAnU1RSSVBFX1NFQ1JFVF9LRVk9JHtTVFJJUEVfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWT0ke1NUUklQRV9TSUdOSU5HX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUPSR7U1RSSVBFX1NJR05JTkdfS0VZX0NPTk5FQ1R9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X2NvbmZpZzovY29uZmlnLycKICAgICAgLSAncG9zdGl6X3VwbG9hZHM6L3VwbG9hZHMvJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTAwMC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTQuNScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rpel9wb3N0Z3Jlc3FsX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj1wb3N0Z3JlcwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQjotcG9zdGl6LWRifScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGl6X3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLWEnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAo=", + "compose": "c2VydmljZXM6CiAgcG9zdGl6OgogICAgaW1hZ2U6ICdnaGNyLmlvL2dpdHJvb21ocS9wb3N0aXotYXBwOnYxLjYwLjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUE9TVElaXzUwMDAKICAgICAgLSAnTUFJTl9VUkw9JHtTRVJWSUNFX0ZRRE5fUE9TVElafScKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0JBQ0tFTkRfVVJMPSR7U0VSVklDRV9GUUROX1BPU1RJWn0vYXBpJwogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1RTRUNSRVR9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovL3Bvc3RncmVzOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfUBwb3N0Z3Jlczo1NDMyLyR7UE9TVEdSRVNRTF9EQVRBQkFTRTotcG9zdGl6LWRifScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdDoke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9QHJlZGlzOjYzNzknCiAgICAgIC0gJ0JBQ0tFTkRfSU5URVJOQUxfVVJMPWh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NPVU5UX0lEPSR7Q0xPVURGTEFSRV9BQ0NPVU5UX0lEfScKICAgICAgLSAnQ0xPVURGTEFSRV9BQ0NFU1NfS0VZPSR7Q0xPVURGTEFSRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnQ0xPVURGTEFSRV9TRUNSRVRfQUNDRVNTX0tFWT0ke0NMT1VERkxBUkVfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVE5BTUU9JHtDTE9VREZMQVJFX0JVQ0tFVE5BTUV9JwogICAgICAtICdDTE9VREZMQVJFX0JVQ0tFVF9VUkw9JHtDTE9VREZMQVJFX0JVQ0tFVF9VUkx9JwogICAgICAtICdDTE9VREZMQVJFX1JFR0lPTj0ke0NMT1VERkxBUkVfUkVHSU9OfScKICAgICAgLSAnU1RPUkFHRV9QUk9WSURFUj0ke1NUT1JBR0VfUFJPVklERVI6LWxvY2FsfScKICAgICAgLSAnVVBMT0FEX0RJUkVDVE9SWT0ke1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX0RJUkVDVE9SWT0ke05FWFRfUFVCTElDX1VQTE9BRF9ESVJFQ1RPUlk6LS91cGxvYWRzfScKICAgICAgLSAnTkVYVF9QVUJMSUNfVVBMT0FEX1NUQVRJQ19ESVJFQ1RPUlk9JHtORVhUX1BVQkxJQ19VUExPQURfU1RBVElDX0RJUkVDVE9SWX0nCiAgICAgIC0gJ1JFU0VORF9BUElfS0VZPSR7UkVTRU5EX0FQSV9LRVl9JwogICAgICAtICdFTUFJTF9GUk9NX0FERFJFU1M9JHtFTUFJTF9GUk9NX0FERFJFU1N9JwogICAgICAtICdFTUFJTF9GUk9NX05BTUU9JHtFTUFJTF9GUk9NX05BTUV9JwogICAgICAtICdFTUFJTF9QUk9WSURFUj0ke0VNQUlMX1BST1ZJREVSfScKICAgICAgLSAnWF9BUElfS0VZPSR7U0VSVklDRV9YX0FQSX0nCiAgICAgIC0gJ1hfQVBJX1NFQ1JFVD0ke1NFUlZJQ0VfWF9TRUNSRVR9JwogICAgICAtICdMSU5LRURJTl9DTElFTlRfSUQ9JHtTRVJWSUNFX0xJTktFRElOX0lEfScKICAgICAgLSAnTElOS0VESU5fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTElOS0VESU5fU0VDUkVUfScKICAgICAgLSAnUkVERElUX0NMSUVOVF9JRD0ke1NFUlZJQ0VfUkVERElUX0FQSX0nCiAgICAgIC0gJ1JFRERJVF9DTElFTlRfU0VDUkVUPSR7U0VSVklDRV9SRURESVRfU0VDUkVUfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9JRD0ke1NFUlZJQ0VfR0lUSFVCX0lEfScKICAgICAgLSAnR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0dJVEhVQl9TRUNSRVR9JwogICAgICAtICdUSFJFQURTX0FQUF9JRD0ke1NFUlZJQ0VfVEhSRUFEU19JRH0nCiAgICAgIC0gJ1RIUkVBRFNfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfVEhSRUFEU19TRUNSRVR9JwogICAgICAtICdGQUNFQk9PS19BUFBfSUQ9JHtTRVJWSUNFX0ZBQ0VCT09LX0lEfScKICAgICAgLSAnRkFDRUJPT0tfQVBQX1NFQ1JFVD0ke1NFUlZJQ0VfRkFDRUJPT0tfU0VDUkVUfScKICAgICAgLSAnWU9VVFVCRV9DTElFTlRfSUQ9JHtTRVJWSUNFX1lPVVRVQkVfSUR9JwogICAgICAtICdZT1VUVUJFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1lPVVRVQkVfU0VDUkVUfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9JRD0ke1NFUlZJQ0VfVElLVE9LX0lEfScKICAgICAgLSAnVElLVE9LX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX1RJS1RPS19TRUNSRVR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX0lEPSR7U0VSVklDRV9QSU5URVJFU1RfSUR9JwogICAgICAtICdQSU5URVJFU1RfQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfUElOVEVSRVNUX1NFQ1JFVH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9JRD0ke1NFUlZJQ0VfRFJJQkJMRV9JRH0nCiAgICAgIC0gJ0RSSUJCQkxFX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RSSUJCTEVfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9DTElFTlRfSUQ9JHtTRVJWSUNFX0RJU0NPUkRfSUR9JwogICAgICAtICdESVNDT1JEX0NMSUVOVF9TRUNSRVQ9JHtTRVJWSUNFX0RJU0NPUkRfU0VDUkVUfScKICAgICAgLSAnRElTQ09SRF9CT1RfVE9LRU5fSUQ9JHtTRVJWSUNFX0RJU0NPUkRfVE9LRU59JwogICAgICAtICdTTEFDS19JRD0ke1NFUlZJQ0VfU0xBQ0tfSUR9JwogICAgICAtICdTTEFDS19TRUNSRVQ9JHtTRVJWSUNFX1NMQUNLX1NFQ1JFVH0nCiAgICAgIC0gJ1NMQUNLX1NJR05JTkdfU0VDUkVUPSR7U0xBQ0tfU0lHTklOR19TRUNSRVR9JwogICAgICAtICdNQVNUT0RPTl9DTElFTlRfSUQ9JHtTRVJWSUNFX01BU1RPRE9OX0lEfScKICAgICAgLSAnTUFTVE9ET05fQ0xJRU5UX1NFQ1JFVD0ke1NFUlZJQ0VfTUFTVE9ET05fU0VDUkVUfScKICAgICAgLSAnQkVFSElJVkVfQVBJX0tFWT0ke1NFUlZJQ0VfQkVFSElJVkVfS0VZfScKICAgICAgLSAnQkVFSElJVkVfUFVCTElDQVRJT05fSUQ9JHtTRVJWSUNFX0JFRUhJSVZFX1BVQklEfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtTRVJWSUNFX09QRU5BSV9LRVl9JwogICAgICAtICdORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlQ9JHtORVhUX1BVQkxJQ19ESVNDT1JEX1NVUFBPUlR9JwogICAgICAtICdORVhUX1BVQkxJQ19QT0xPVE5PPSR7TkVYVF9QVUJMSUNfUE9MT1ROT30nCiAgICAgIC0gSVNfR0VORVJBTD10cnVlCiAgICAgIC0gJ05YX0FERF9QTFVHSU5TPSR7TlhfQUREX1BMVUdJTlM6LWZhbHNlfScKICAgICAgLSAnRkVFX0FNT1VOVD0ke0ZFRV9BTU9VTlQ6LTAuMDV9JwogICAgICAtICdTVFJJUEVfUFVCTElTSEFCTEVfS0VZPSR7U1RSSVBFX1BVQkxJU0hBQkxFX0tFWX0nCiAgICAgIC0gJ1NUUklQRV9TRUNSRVRfS0VZPSR7U1RSSVBFX1NFQ1JFVF9LRVl9JwogICAgICAtICdTVFJJUEVfU0lHTklOR19LRVk9JHtTVFJJUEVfU0lHTklOR19LRVl9JwogICAgICAtICdTVFJJUEVfU0lHTklOR19LRVlfQ09OTkVDVD0ke1NUUklQRV9TSUdOSU5HX0tFWV9DT05ORUNUfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rpel9jb25maWc6L2NvbmZpZy8nCiAgICAgIC0gJ3Bvc3Rpel91cGxvYWRzOi91cGxvYWRzLycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjUwMDAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE0LjUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0aXpfcG9zdGdyZXNxbF9kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9cG9zdGdyZXMKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXBvc3Rpei1kYn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREI6LXBvc3Rpei1kYn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1JFRElTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tcmVxdWlyZXBhc3MgJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3Rpel9yZWRpc19kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gJy1hJwogICAgICAgIC0gJyR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAK", "tags": [ "post everywhere", "social media", From 6a90bdf9fa4984d327b4353ebaac62f474c719bc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:30:44 +0200 Subject: [PATCH 11/18] fix(env): Generate literal env variables better --- app/Jobs/ApplicationDeploymentJob.php | 24 ++---------------------- app/Models/EnvironmentVariable.php | 9 ++++++++- app/Models/Service.php | 24 ++++++++---------------- bootstrap/helpers/shared.php | 5 ++--- 4 files changed, 20 insertions(+), 42 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c9b7c9992..f3ad4383c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -908,17 +908,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->env_filename = '.env'; foreach ($sorted_environment_variables as $env) { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $envs->push($env->key.'='.$real_value); + $envs->push($env->key.'='.$env->real_value); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { @@ -933,17 +923,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->env_filename = ".env-pr-$this->pull_request_id"; foreach ($sorted_environment_variables_preview as $env) { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $envs->push($env->key.'='.$real_value); + $envs->push($env->key.'='.$env->real_value); } // Add PORT if not exists, use the first port as default if ($this->build_pack !== 'dockercompose') { diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 04081fce0..b8bde5c84 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -118,7 +118,14 @@ class EnvironmentVariable extends BaseModel return null; } - return $this->get_real_environment_variables($this->value, $resource); + $real_value = $this->get_real_environment_variables($this->value, $resource); + if ($this->is_literal || $this->is_multiline) { + $real_value = '\''.$real_value.'\''; + } else { + $real_value = escapeEnvVariables($real_value); + } + + return $real_value; } ); } diff --git a/app/Models/Service.php b/app/Models/Service.php index a9302d7e7..8f0c16d40 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1260,26 +1260,18 @@ class Service extends BaseModel return 3; }); + $envs = collect([]); foreach ($sorted as $env) { - if (version_compare($env->version, '4.0.0-beta.347', '<=')) { - $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; - } else { - $real_value = $env->real_value; - if ($env->version === '4.0.0-beta.239') { - $real_value = $env->real_value; - } else { - if ($env->is_literal || $env->is_multiline) { - $real_value = '\''.$real_value.'\''; - } else { - $real_value = escapeEnvVariables($env->real_value); - } - } - $commands[] = "echo \"{$env->key}={$real_value}\" >> .env"; - } + $envs->push("{$env->key}={$env->real_value}"); } - if ($sorted->count() === 0) { + ray($envs); + if ($envs->count() === 0) { $commands[] = 'touch .env'; + } else { + $envs_base64 = base64_encode($envs->implode("\n")); + $commands[] = "echo '$envs_base64' | base64 -d | tee .env > /dev/null"; } + instant_remote_process($commands, $this->server); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 3685dcfd5..2fcc44ac9 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2946,7 +2946,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } catch (\Exception) { return collect([]); } - $services = data_get($yaml, 'services', collect([])); $topLevel = collect([ 'volumes' => collect(data_get($yaml, 'volumes', [])), @@ -3064,7 +3063,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } } - // Get magic environments where we need to preset the FQDN if ($key->startsWith('SERVICE_FQDN_')) { // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 @@ -3614,7 +3612,8 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'is_required' => $isRequired, ]); // Add the variable to the environment so it will be shown in the deployable compose file - $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->value; + // $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->real_value; + $environment[$parsedKeyValue->value()] = $value; continue; } From e9ca8c355919971a90b8ce64327495504009bb91 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:55:23 +0200 Subject: [PATCH 12/18] fix(deployment): update x-data initialization in deployment view for improved functionality --- .../livewire/project/application/deployment/show.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/project/application/deployment/show.blade.php b/resources/views/livewire/project/application/deployment/show.blade.php index 01edc44e2..0857818d6 100644 --- a/resources/views/livewire/project/application/deployment/show.blade.php +++ b/resources/views/livewire/project/application/deployment/show.blade.php @@ -5,7 +5,7 @@

Deployment

-
Date: Mon, 7 Jul 2025 12:55:35 +0200 Subject: [PATCH 13/18] fix(deployment): enhance COOLIFY_URL and COOLIFY_FQDN variable generation for better compatibility --- app/Jobs/ApplicationDeploymentJob.php | 13 ++++++++++--- app/Models/Service.php | 1 - bootstrap/helpers/shared.php | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index f3ad4383c..c4a71d599 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1347,9 +1347,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $fqdn = $this->preview->fqdn; } if (isset($fqdn)) { - $this->coolify_variables .= "COOLIFY_FQDN={$fqdn} "; - $url = str($fqdn)->replace('http://', '')->replace('https://', ''); - $this->coolify_variables .= "COOLIFY_URL={$url} "; + $fqdnWithoutPort = str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); + $url = str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); + if ((int) $this->application->compose_parsing_version >= 3) { + $this->coolify_variables .= "COOLIFY_URL={$fqdnWithoutPort} "; + $this->coolify_variables .= "COOLIFY_FQDN={$url} "; + } else { + $this->coolify_variables .= "COOLIFY_URL={$fqdnWithoutPort} "; + $this->coolify_variables .= "COOLIFY_FQDN={$url} "; + } + } if (isset($this->application->git_branch)) { $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} "; diff --git a/app/Models/Service.php b/app/Models/Service.php index 8f0c16d40..da6c34fbb 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1264,7 +1264,6 @@ class Service extends BaseModel foreach ($sorted as $env) { $envs->push("{$env->key}={$env->real_value}"); } - ray($envs); if ($envs->count() === 0) { $commands[] = 'touch .env'; } else { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 2fcc44ac9..b8497e2d5 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3716,10 +3716,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } // Add COOLIFY_FQDN & COOLIFY_URL to environment if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { - $coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(',')); + $fqdnsWithoutPort = $fqdns->map(function ($fqdn) { + return str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); + }); + $coolifyEnvironments->put('COOLIFY_URL', $fqdnsWithoutPort->implode(',')); $urls = $fqdns->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', ''); + return str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); }); $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(',')); } From 6d94aaf0f872724e62c984251f55366801c41fce Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 13:14:45 +0200 Subject: [PATCH 14/18] refactor(previews): streamline preview URL generation by utilizing application method --- app/Livewire/Project/Application/Previews.php | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index b2c1cf8e1..c781c9d8a 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -7,7 +7,6 @@ use App\Models\Application; use App\Models\ApplicationPreview; use Illuminate\Support\Collection; use Livewire\Component; -use Spatie\Url\Url; use Visus\Cuid2\Cuid2; class Previews extends Component @@ -87,18 +86,9 @@ class Previews extends Component return; } - $fqdn = generateFqdn($this->application->destination->server, $this->application->uuid); - $url = Url::fromString($fqdn); - $template = $this->application->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $preview->fqdn = $preview_fqdn; - $preview->save(); + $this->application->generate_preview_fqdn($preview->pull_request_id); + $this->application->refresh(); + $this->dispatch('update_links'); $this->dispatch('success', 'Domain generated.'); } From 734715e8f8d07e1177f70519a9f117376157f3ff Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:06:28 +0200 Subject: [PATCH 15/18] refactor(application): adjust layout and spacing in general application view for improved UI --- .../livewire/project/application/general.blade.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 91699804d..39102c39b 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -69,7 +69,7 @@ Generate Default Nginx Configuration @endif -
+
@if ($application->could_set_build_commands()) @@ -176,7 +176,7 @@ @endif
@endif -
+

Build

@if ($application->build_pack === 'dockerimage') @if ($application->build_pack === 'dockercompose') - Reload Compose File +
+

Docker Compose

+ Reload Compose File +
@if ($application->settings->is_raw_compose_deployment_enabled) Date: Mon, 7 Jul 2025 16:07:08 +0200 Subject: [PATCH 16/18] fix(deployment): improve docker-compose domain handling and environment variable generation --- app/Jobs/ApplicationDeploymentJob.php | 54 +++++++++++++++++++++------ app/Models/ApplicationPreview.php | 50 ++++++++++++++++--------- bootstrap/helpers/shared.php | 31 +++++++++++++-- 3 files changed, 103 insertions(+), 32 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c4a71d599..109c72884 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -471,7 +471,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $this->save_environment_variables(); - if (! is_null($this->env_filename)) { + if (filled($this->env_filename)) { $services = collect(data_get($composeFile, 'services', [])); $services = $services->map(function ($service, $name) { $service['env_file'] = [$this->env_filename]; @@ -480,7 +480,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue }); $composeFile['services'] = $services->toArray(); } - if (is_null($composeFile)) { + if (empty($composeFile)) { $this->application_deployment_queue->addLogEntry('Failed to parse docker-compose file.'); $this->fail('Failed to parse docker-compose file.'); @@ -887,10 +887,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private function save_environment_variables() { $envs = collect([]); - $local_branch = $this->branch; - if ($this->pull_request_id !== 0) { - $local_branch = "pull/{$this->pull_request_id}/head"; - } $sort = $this->application->settings->is_env_sorting_enabled; if ($sort) { $sorted_environment_variables = $this->application->environment_variables->sortBy('key'); @@ -899,6 +895,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $sorted_environment_variables = $this->application->environment_variables->sortBy('id'); $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id'); } + if ($this->build_pack === 'dockercompose') { + $sorted_environment_variables = $sorted_environment_variables->filter(function ($env) { + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + }); + $sorted_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) { + return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_'); + }); + } $ports = $this->application->main_port(); $coolify_envs = $this->generate_coolify_env_variables(); $coolify_envs->each(function ($item, $key) use ($envs) { @@ -920,6 +924,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push('HOST=0.0.0.0'); } + + if ($this->build_pack === 'dockercompose') { + $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect([]); + + // Generate SERVICE_FQDN & SERVICE_URL for dockercompose + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = str($parsedDomain)->after('://')->before(':')->prepend(str($parsedDomain)->before('://')->append('://')); + $coolifyFqdn = str($parsedDomain)->replace('http://', '')->replace('https://', '')->before(':'); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->value()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn->value()); + } + } + } } else { $this->env_filename = ".env-pr-$this->pull_request_id"; foreach ($sorted_environment_variables_preview as $env) { @@ -936,6 +956,21 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $envs->push('HOST=0.0.0.0'); } + if ($this->build_pack === 'dockercompose') { + $domains = collect(json_decode(data_get($this->preview, 'docker_compose_domains'))) ?? collect([]); + + // Generate SERVICE_FQDN & SERVICE_URL for dockercompose + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = str($parsedDomain)->after('://')->before(':')->prepend(str($parsedDomain)->before('://')->append('://')); + $coolifyFqdn = str($parsedDomain)->replace('http://', '')->replace('https://', '')->before(':'); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->value()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn->value()); + } + } + } } if ($envs->isEmpty()) { $this->env_filename = null; @@ -1702,10 +1737,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue { $this->create_workdir(); $ports = $this->application->main_port(); - $onlyPort = null; - if (count($ports) > 0) { - $onlyPort = $ports[0]; - } $persistent_storages = $this->generate_local_persistent_volumes(); $persistent_file_volumes = $this->application->fileStorages()->get(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names(); @@ -2240,9 +2271,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->application_deployment_queue->addLogEntry('Building docker image completed.'); } - private function graceful_shutdown_container(string $containerName, int $timeout = 30) + private function graceful_shutdown_container(string $containerName) { try { + $timeout = isDev() ? 1 : 30; $this->execute_remote_command( ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index bf2bf05bf..c635f146a 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -52,24 +52,38 @@ class ApplicationPreview extends BaseModel public function generate_preview_fqdn_compose() { - $domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect(); - foreach ($domains as $service_name => $domain) { - $domain = data_get($domain, 'domain'); - $url = Url::fromString($domain); - $template = $this->application->preview_url_template; - $host = $url->getHost(); - $schema = $url->getScheme(); - $random = new Cuid2; - $preview_fqdn = str_replace('{{random}}', $random, $template); - $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); - $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); - $preview_fqdn = "$schema://$preview_fqdn"; - $docker_compose_domains = data_get($this, 'docker_compose_domains'); - $docker_compose_domains = json_decode($docker_compose_domains, true); - $docker_compose_domains[$service_name]['domain'] = $preview_fqdn; - $docker_compose_domains = json_encode($docker_compose_domains); - $this->docker_compose_domains = $docker_compose_domains; - $this->save(); + $services = collect(json_decode($this->application->docker_compose_domains)) ?? collect(); + $docker_compose_domains = data_get($this, 'docker_compose_domains'); + $docker_compose_domains = json_decode($docker_compose_domains, true) ?? []; + + foreach ($services as $service_name => $service_config) { + $domain_string = data_get($service_config, 'domain'); + $service_domains = str($domain_string)->explode(',')->map(fn ($d) => trim($d)); + + $preview_domains = []; + foreach ($service_domains as $domain) { + if (empty($domain)) { + continue; + } + + $url = Url::fromString($domain); + $template = $this->application->preview_url_template; + $host = $url->getHost(); + $schema = $url->getScheme(); + $random = new Cuid2; + $preview_fqdn = str_replace('{{random}}', $random, $template); + $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); + $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); + $preview_fqdn = "$schema://$preview_fqdn"; + $preview_domains[] = $preview_fqdn; + } + + if (! empty($preview_domains)) { + $docker_compose_domains[$service_name]['domain'] = implode(',', $preview_domains); + } } + + $this->docker_compose_domains = json_encode($docker_compose_domains); + $this->save(); } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index b8497e2d5..e62185cc1 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3147,6 +3147,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int continue; } if ($command->value() === 'FQDN') { + if ($isApplication && $resource->build_pack === 'dockercompose') { + continue; + } $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); if (str($fqdnFor)->contains('_')) { $fqdnFor = str($fqdnFor)->before('_'); @@ -3162,6 +3165,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'is_preview' => false, ]); } elseif ($command->value() === 'URL') { + if ($isApplication && $resource->build_pack === 'dockercompose') { + continue; + } $fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); if (str($fqdnFor)->contains('_')) { $fqdnFor = str($fqdnFor)->before('_'); @@ -3651,9 +3657,28 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } if ($isApplication) { - $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); + if ($isPullRequest) { + $preview = $resource->previews()->find($preview_id); + $domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))) ?? collect([]); + } else { + $domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]); + } $fqdns = data_get($domains, "$serviceName.domain"); - if ($fqdns) { + // Generate SERVICE_FQDN & SERVICE_URL for dockercompose + if ($resource->build_pack === 'dockercompose') { + foreach ($domains as $forServiceName => $domain) { + $parsedDomain = data_get($domain, 'domain'); + if (filled($parsedDomain)) { + $parsedDomain = str($parsedDomain)->explode(',')->first(); + $coolifyUrl = str($parsedDomain)->after('://')->before(':')->prepend(str($parsedDomain)->before('://')->append('://')); + $coolifyFqdn = str($parsedDomain)->replace('http://', '')->replace('https://', '')->before(':'); + $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper(), $coolifyUrl->value()); + $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper(), $coolifyFqdn->value()); + } + } + } + // If the domain is set, we need to generate the FQDNs for the preview + if (filled($fqdns)) { $fqdns = str($fqdns)->explode(','); if ($isPullRequest) { $preview = $resource->previews()->find($preview_id); @@ -3685,7 +3710,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } } - $defaultLabels = defaultLabels( id: $resource->id, name: $containerName, @@ -3695,6 +3719,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int type: 'application', environment: $resource->environment->name, ); + } elseif ($isService) { if ($savedService->serviceType()) { $fqdns = generateServiceSpecificFqdns($savedService); From 94f9c542560992b193d59617a6e019319e4d2c1f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:42:34 +0200 Subject: [PATCH 17/18] fix(deployment): refactor domain parsing and environment variable generation using Spatie URL library --- app/Jobs/ApplicationDeploymentJob.php | 21 +++++++++++++-------- bootstrap/helpers/shared.php | 10 ++++++---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 109c72884..c31a64fa1 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -30,6 +30,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Sleep; use Illuminate\Support\Str; use RuntimeException; +use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Throwable; use Visus\Cuid2\Cuid2; @@ -933,10 +934,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $parsedDomain = data_get($domain, 'domain'); if (filled($parsedDomain)) { $parsedDomain = str($parsedDomain)->explode(',')->first(); - $coolifyUrl = str($parsedDomain)->after('://')->before(':')->prepend(str($parsedDomain)->before('://')->append('://')); - $coolifyFqdn = str($parsedDomain)->replace('http://', '')->replace('https://', '')->before(':'); - $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->value()); - $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn->value()); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); } } } @@ -964,10 +967,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $parsedDomain = data_get($domain, 'domain'); if (filled($parsedDomain)) { $parsedDomain = str($parsedDomain)->explode(',')->first(); - $coolifyUrl = str($parsedDomain)->after('://')->before(':')->prepend(str($parsedDomain)->before('://')->append('://')); - $coolifyFqdn = str($parsedDomain)->replace('http://', '')->replace('https://', '')->before(':'); - $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->value()); - $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn->value()); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString()); + $envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn); } } } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index e62185cc1..00a674eeb 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -3670,10 +3670,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $parsedDomain = data_get($domain, 'domain'); if (filled($parsedDomain)) { $parsedDomain = str($parsedDomain)->explode(',')->first(); - $coolifyUrl = str($parsedDomain)->after('://')->before(':')->prepend(str($parsedDomain)->before('://')->append('://')); - $coolifyFqdn = str($parsedDomain)->replace('http://', '')->replace('https://', '')->before(':'); - $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper(), $coolifyUrl->value()); - $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper(), $coolifyFqdn->value()); + $coolifyUrl = Url::fromString($parsedDomain); + $coolifyScheme = $coolifyUrl->getScheme(); + $coolifyFqdn = $coolifyUrl->getHost(); + $coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null); + $coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper(), $coolifyUrl->__toString()); + $coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper(), $coolifyFqdn); } } } From b9ba04c4e157220dd8483e30153ef95e50bebb09 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:49:09 +0200 Subject: [PATCH 18/18] fix(deployment): update COOLIFY_URL and COOLIFY_FQDN generation to use Spatie URL library for improved accuracy --- app/Jobs/ApplicationDeploymentJob.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index c31a64fa1..bc5fab30c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1387,16 +1387,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $fqdn = $this->preview->fqdn; } if (isset($fqdn)) { - $fqdnWithoutPort = str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://')); - $url = str($fqdn)->replace('http://', '')->replace('https://', '')->before(':'); + $url = Url::fromString($fqdn); + $fqdn = $url->getHost(); + $url = $url->withHost($fqdn)->withPort(null)->__toString(); if ((int) $this->application->compose_parsing_version >= 3) { - $this->coolify_variables .= "COOLIFY_URL={$fqdnWithoutPort} "; - $this->coolify_variables .= "COOLIFY_FQDN={$url} "; + $this->coolify_variables .= "COOLIFY_URL={$url} "; + $this->coolify_variables .= "COOLIFY_FQDN={$fqdn} "; } else { - $this->coolify_variables .= "COOLIFY_URL={$fqdnWithoutPort} "; + $this->coolify_variables .= "COOLIFY_URL={$fqdn} "; $this->coolify_variables .= "COOLIFY_FQDN={$url} "; } - } if (isset($this->application->git_branch)) { $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";