Merge pull request #6146 from coollabsio/next

v4.0.0-beta.420.4
This commit is contained in:
Andras Bacsai
2025-07-08 10:52:30 +02:00
committed by GitHub
17 changed files with 475 additions and 156 deletions

View File

@@ -47,7 +47,6 @@ class StartDatabaseProxy
if ($isSSLEnabled) { if ($isSSLEnabled) {
$internalPort = match ($databaseType) { $internalPort = match ($databaseType) {
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6380, 'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6380,
default => throw new \Exception("Unsupported database type: $databaseType"),
}; };
} }

View File

@@ -7,26 +7,270 @@ use Illuminate\Support\Facades\Redis;
class CleanupRedis extends Command 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() public function handle()
{ {
$redis = Redis::connection('horizon'); $redis = Redis::connection('horizon');
$keys = $redis->keys('*');
$prefix = config('horizon.prefix'); $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) { foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key); $keyWithoutPrefix = str_replace($prefix, '', $key);
$type = $redis->command('type', [$keyWithoutPrefix]); $type = $redis->command('type', [$keyWithoutPrefix]);
// Handle hash-type keys (individual jobs)
if ($type === 5) { if ($type === 5) {
$data = $redis->command('hgetall', [$keyWithoutPrefix]); if ($this->shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun)) {
$status = data_get($data, 'status'); $deletedCount++;
if ($status === 'completed') { }
$redis->command('del', [$keyWithoutPrefix]); }
// 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;
} }
} }

View File

@@ -36,24 +36,20 @@ class Init extends Command
$this->servers = Server::all(); $this->servers = Server::all();
if (! isCloud()) { if (! isCloud()) {
$this->send_alive_signal(); $this->sendAliveSignal();
get_public_ips(); get_public_ips();
} }
// Backward compatibility // Backward compatibility
$this->replace_slash_in_environment_name(); $this->replaceSlashInEnvironmentName();
$this->restore_coolify_db_backup(); $this->restoreCoolifyDbBackup();
$this->update_user_emails(); $this->updateUserEmails();
// //
$this->update_traefik_labels(); $this->updateTraefikLabels();
if (! isCloud() || $this->option('force-cloud')) { if (! isCloud() || $this->option('force-cloud')) {
$this->cleanup_unused_network_from_coolify_proxy(); $this->cleanupUnusedNetworkFromCoolifyProxy();
}
if (isCloud()) {
$this->cleanup_unnecessary_dynamic_proxy_configuration();
} else {
$this->cleanup_in_progress_application_deployments();
} }
$this->call('cleanup:redis'); $this->call('cleanup:redis');
$this->call('cleanup:stucked-resources'); $this->call('cleanup:stucked-resources');
@@ -66,33 +62,35 @@ class Init extends Command
if (isCloud()) { if (isCloud()) {
try { try {
$this->cleanupUnnecessaryDynamicProxyConfiguration();
$this->pullTemplatesFromCDN(); $this->pullTemplatesFromCDN();
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n"; echo "Could not pull templates from CDN: {$e->getMessage()}\n";
} }
return;
} }
if (! isCloud()) { try {
try { $this->cleanupInProgressApplicationDeployments();
$this->pullTemplatesFromCDN(); $this->pullTemplatesFromCDN();
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not pull templates from CDN: {$e->getMessage()}\n"; echo "Could not pull templates from CDN: {$e->getMessage()}\n";
} }
try { try {
$localhost = $this->servers->where('id', 0)->first(); $localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration(); $localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
} }
$settings = instanceSettings(); $settings = instanceSettings();
if (! is_null(config('constants.coolify.autoupdate', null))) { if (! is_null(config('constants.coolify.autoupdate', null))) {
if (config('constants.coolify.autoupdate') == true) { if (config('constants.coolify.autoupdate') == true) {
echo "Enabling auto-update\n"; echo "Enabling auto-update\n";
$settings->update(['is_auto_update_enabled' => true]); $settings->update(['is_auto_update_enabled' => true]);
} else { } else {
echo "Disabling auto-update\n"; echo "Disabling auto-update\n";
$settings->update(['is_auto_update_enabled' => false]); $settings->update(['is_auto_update_enabled' => false]);
}
} }
} }
} }
@@ -117,7 +115,7 @@ class Init extends Command
Artisan::call('optimize'); Artisan::call('optimize');
} }
private function update_user_emails() private function updateUserEmails()
{ {
try { try {
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) { 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 { try {
Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']); 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) { foreach ($this->servers as $server) {
try { 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) { foreach ($this->servers as $server) {
if (! $server->isFunctional()) { 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'), '<=')) { if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try { try {
@@ -223,7 +221,7 @@ class Init extends Command
} }
} }
private function send_alive_signal() private function sendAliveSignal()
{ {
$id = config('app.id'); $id = config('app.id');
$version = config('constants.coolify.version'); $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 // Cleanup any failed deployments
try { 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'), '<=')) { if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all(); $environments = Environment::all();

View File

@@ -30,6 +30,7 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Sleep; use Illuminate\Support\Sleep;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Throwable; use Throwable;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -471,7 +472,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} else { } else {
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id')); $composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables(); $this->save_environment_variables();
if (! is_null($this->env_filename)) { if (filled($this->env_filename)) {
$services = collect(data_get($composeFile, 'services', [])); $services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) { $services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename]; $service['env_file'] = [$this->env_filename];
@@ -480,7 +481,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}); });
$composeFile['services'] = $services->toArray(); $composeFile['services'] = $services->toArray();
} }
if (is_null($composeFile)) { if (empty($composeFile)) {
$this->application_deployment_queue->addLogEntry('Failed to parse docker-compose file.'); $this->application_deployment_queue->addLogEntry('Failed to parse docker-compose file.');
$this->fail('Failed to parse docker-compose file.'); $this->fail('Failed to parse docker-compose file.');
@@ -887,10 +888,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function save_environment_variables() private function save_environment_variables()
{ {
$envs = collect([]); $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; $sort = $this->application->settings->is_env_sorting_enabled;
if ($sort) { if ($sort) {
$sorted_environment_variables = $this->application->environment_variables->sortBy('key'); $sorted_environment_variables = $this->application->environment_variables->sortBy('key');
@@ -899,6 +896,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$sorted_environment_variables = $this->application->environment_variables->sortBy('id'); $sorted_environment_variables = $this->application->environment_variables->sortBy('id');
$sorted_environment_variables_preview = $this->application->environment_variables_preview->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(); $ports = $this->application->main_port();
$coolify_envs = $this->generate_coolify_env_variables(); $coolify_envs = $this->generate_coolify_env_variables();
$coolify_envs->each(function ($item, $key) use ($envs) { $coolify_envs->each(function ($item, $key) use ($envs) {
@@ -908,17 +913,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->env_filename = '.env'; $this->env_filename = '.env';
foreach ($sorted_environment_variables as $env) { foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value; $envs->push($env->key.'='.$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);
} }
// Add PORT if not exists, use the first port as default // Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') { if ($this->build_pack !== 'dockercompose') {
@@ -930,20 +925,28 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0'); $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 = 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);
}
}
}
} else { } else {
$this->env_filename = ".env-pr-$this->pull_request_id"; $this->env_filename = ".env-pr-$this->pull_request_id";
foreach ($sorted_environment_variables_preview as $env) { foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value; $envs->push($env->key.'='.$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);
} }
// Add PORT if not exists, use the first port as default // Add PORT if not exists, use the first port as default
if ($this->build_pack !== 'dockercompose') { if ($this->build_pack !== 'dockercompose') {
@@ -956,6 +959,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push('HOST=0.0.0.0'); $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 = 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);
}
}
}
} }
if ($envs->isEmpty()) { if ($envs->isEmpty()) {
$this->env_filename = null; $this->env_filename = null;
@@ -1367,9 +1387,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$fqdn = $this->preview->fqdn; $fqdn = $this->preview->fqdn;
} }
if (isset($fqdn)) { if (isset($fqdn)) {
$this->coolify_variables .= "COOLIFY_FQDN={$fqdn} "; $url = Url::fromString($fqdn);
$url = str($fqdn)->replace('http://', '')->replace('https://', ''); $fqdn = $url->getHost();
$this->coolify_variables .= "COOLIFY_URL={$url} "; $url = $url->withHost($fqdn)->withPort(null)->__toString();
if ((int) $this->application->compose_parsing_version >= 3) {
$this->coolify_variables .= "COOLIFY_URL={$url} ";
$this->coolify_variables .= "COOLIFY_FQDN={$fqdn} ";
} else {
$this->coolify_variables .= "COOLIFY_URL={$fqdn} ";
$this->coolify_variables .= "COOLIFY_FQDN={$url} ";
}
} }
if (isset($this->application->git_branch)) { if (isset($this->application->git_branch)) {
$this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} "; $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";
@@ -1715,10 +1742,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{ {
$this->create_workdir(); $this->create_workdir();
$ports = $this->application->main_port(); $ports = $this->application->main_port();
$onlyPort = null;
if (count($ports) > 0) {
$onlyPort = $ports[0];
}
$persistent_storages = $this->generate_local_persistent_volumes(); $persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->application->fileStorages()->get(); $persistent_file_volumes = $this->application->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names(); $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
@@ -2253,9 +2276,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.'); $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 { try {
$timeout = isDev() ? 1 : 30;
$this->execute_remote_command( $this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]

View File

@@ -7,7 +7,6 @@ use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
class Previews extends Component class Previews extends Component
@@ -87,18 +86,9 @@ class Previews extends Component
return; return;
} }
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid); $this->application->generate_preview_fqdn($preview->pull_request_id);
$url = Url::fromString($fqdn); $this->application->refresh();
$template = $this->application->preview_url_template; $this->dispatch('update_links');
$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->dispatch('success', 'Domain generated.'); $this->dispatch('success', 'Domain generated.');
} }

View File

@@ -121,8 +121,8 @@ class BackupExecutions extends Component
{ {
return view('livewire.project.database.backup-executions', [ return view('livewire.project.database.backup-executions', [
'checkboxes' => [ 'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'], ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently from S3 Storage'],
// ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'], // ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently from SFTP Storage'],
], ],
]); ]);
} }

View File

@@ -52,24 +52,38 @@ class ApplicationPreview extends BaseModel
public function generate_preview_fqdn_compose() public function generate_preview_fqdn_compose()
{ {
$domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect(); $services = collect(json_decode($this->application->docker_compose_domains)) ?? collect();
foreach ($domains as $service_name => $domain) { $docker_compose_domains = data_get($this, 'docker_compose_domains');
$domain = data_get($domain, 'domain'); $docker_compose_domains = json_decode($docker_compose_domains, true) ?? [];
$url = Url::fromString($domain);
$template = $this->application->preview_url_template; foreach ($services as $service_name => $service_config) {
$host = $url->getHost(); $domain_string = data_get($service_config, 'domain');
$schema = $url->getScheme(); $service_domains = str($domain_string)->explode(',')->map(fn ($d) => trim($d));
$random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template); $preview_domains = [];
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); foreach ($service_domains as $domain) {
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); if (empty($domain)) {
$preview_fqdn = "$schema://$preview_fqdn"; continue;
$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; $url = Url::fromString($domain);
$docker_compose_domains = json_encode($docker_compose_domains); $template = $this->application->preview_url_template;
$this->docker_compose_domains = $docker_compose_domains; $host = $url->getHost();
$this->save(); $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();
} }
} }

View File

@@ -118,7 +118,14 @@ class EnvironmentVariable extends BaseModel
return null; 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;
} }
); );
} }

View File

@@ -1260,26 +1260,17 @@ class Service extends BaseModel
return 3; return 3;
}); });
$envs = collect([]);
foreach ($sorted as $env) { foreach ($sorted as $env) {
if (version_compare($env->version, '4.0.0-beta.347', '<=')) { $envs->push("{$env->key}={$env->real_value}");
$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";
}
} }
if ($sorted->count() === 0) { if ($envs->count() === 0) {
$commands[] = 'touch .env'; $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); instant_remote_process($commands, $this->server);
} }

View File

@@ -599,7 +599,15 @@ function getTopLevelNetworks(Service|Application $resource)
try { try {
$yaml = Yaml::parse($resource->docker_compose_raw); $yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) { } 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'); $services = data_get($yaml, 'services');
$topLevelNetworks = collect(data_get($yaml, 'networks', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
@@ -653,9 +661,16 @@ function getTopLevelNetworks(Service|Application $resource)
try { try {
$yaml = Yaml::parse($resource->docker_compose_raw); $yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) { } 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', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
$services = data_get($yaml, 'services'); $services = data_get($yaml, 'services');
$definedNetwork = collect([$resource->uuid]); $definedNetwork = collect([$resource->uuid]);
@@ -2931,7 +2946,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} catch (\Exception) { } catch (\Exception) {
return collect([]); return collect([]);
} }
$services = data_get($yaml, 'services', collect([])); $services = data_get($yaml, 'services', collect([]));
$topLevel = collect([ $topLevel = collect([
'volumes' => collect(data_get($yaml, 'volumes', [])), 'volumes' => collect(data_get($yaml, 'volumes', [])),
@@ -3049,7 +3063,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} }
} }
} }
// Get magic environments where we need to preset the FQDN // Get magic environments where we need to preset the FQDN
if ($key->startsWith('SERVICE_FQDN_')) { if ($key->startsWith('SERVICE_FQDN_')) {
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000 // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
@@ -3134,6 +3147,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
continue; continue;
} }
if ($command->value() === 'FQDN') { if ($command->value() === 'FQDN') {
if ($isApplication && $resource->build_pack === 'dockercompose') {
continue;
}
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value(); $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
if (str($fqdnFor)->contains('_')) { if (str($fqdnFor)->contains('_')) {
$fqdnFor = str($fqdnFor)->before('_'); $fqdnFor = str($fqdnFor)->before('_');
@@ -3149,6 +3165,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'is_preview' => false, 'is_preview' => false,
]); ]);
} elseif ($command->value() === 'URL') { } elseif ($command->value() === 'URL') {
if ($isApplication && $resource->build_pack === 'dockercompose') {
continue;
}
$fqdnFor = $key->after('SERVICE_URL_')->lower()->value(); $fqdnFor = $key->after('SERVICE_URL_')->lower()->value();
if (str($fqdnFor)->contains('_')) { if (str($fqdnFor)->contains('_')) {
$fqdnFor = str($fqdnFor)->before('_'); $fqdnFor = str($fqdnFor)->before('_');
@@ -3599,7 +3618,8 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'is_required' => $isRequired, 'is_required' => $isRequired,
]); ]);
// Add the variable to the environment so it will be shown in the deployable compose file // 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; continue;
} }
@@ -3637,9 +3657,30 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} }
if ($isApplication) { 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"); $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 = 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);
}
}
}
// If the domain is set, we need to generate the FQDNs for the preview
if (filled($fqdns)) {
$fqdns = str($fqdns)->explode(','); $fqdns = str($fqdns)->explode(',');
if ($isPullRequest) { if ($isPullRequest) {
$preview = $resource->previews()->find($preview_id); $preview = $resource->previews()->find($preview_id);
@@ -3671,7 +3712,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} }
} }
} }
$defaultLabels = defaultLabels( $defaultLabels = defaultLabels(
id: $resource->id, id: $resource->id,
name: $containerName, name: $containerName,
@@ -3681,6 +3721,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
type: 'application', type: 'application',
environment: $resource->environment->name, environment: $resource->environment->name,
); );
} elseif ($isService) { } elseif ($isService) {
if ($savedService->serviceType()) { if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService); $fqdns = generateServiceSpecificFqdns($savedService);
@@ -3702,10 +3743,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} }
// Add COOLIFY_FQDN & COOLIFY_URL to environment // Add COOLIFY_FQDN & COOLIFY_URL to environment
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { 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) { $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(',')); $coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(','));
} }

View File

@@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.420.3', 'version' => '4.0.0-beta.420.4',
'helper_version' => '1.0.8', 'helper_version' => '1.0.8',
'realtime_version' => '1.0.9', 'realtime_version' => '1.0.9',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),

View File

@@ -5,7 +5,7 @@
<h1 class="py-0">Deployment</h1> <h1 class="py-0">Deployment</h1>
<livewire:project.shared.configuration-checker :resource="$application" /> <livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" /> <livewire:project.application.heading :application="$application" />
<div class="pt-4" x-data="{ <div x-data="{
fullscreen: false, fullscreen: false,
alwaysScroll: false, alwaysScroll: false,
intervalId: null, intervalId: null,

View File

@@ -69,7 +69,7 @@
<x-forms.button wire:click="generateNginxConfiguration">Generate Default Nginx <x-forms.button wire:click="generateNginxConfiguration">Generate Default Nginx
Configuration</x-forms.button> Configuration</x-forms.button>
@endif @endif
<div class="w-96 pb-8"> <div class="w-96 pb-6">
@if ($application->could_set_build_commands()) @if ($application->could_set_build_commands())
<x-forms.checkbox instantSave id="application.settings.is_static" label="Is it a static site?" <x-forms.checkbox instantSave id="application.settings.is_static" label="Is it a static site?"
helper="If your application is a static site or the final build assets should be served as a static site, enable this." /> helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
@@ -176,7 +176,7 @@
@endif @endif
</div> </div>
@endif @endif
<div class="py-4 border-b dark:border-coolgray-200"> <div>
<h3>Build</h3> <h3>Build</h3>
@if ($application->build_pack === 'dockerimage') @if ($application->build_pack === 'dockerimage')
<x-forms.input <x-forms.input
@@ -290,8 +290,11 @@
@endif @endif
</div> </div>
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.button wire:target='initLoadingCompose' <div class="flex items-center gap-2 pb-4">
x-on:click="$wire.dispatch('loadCompose', false)">Reload Compose File</x-forms.button> <h3>Docker Compose</h3>
<x-forms.button wire:target='initLoadingCompose'
x-on:click="$wire.dispatch('loadCompose', false)">Reload Compose File</x-forms.button>
</div>
@if ($application->settings->is_raw_compose_deployment_enabled) @if ($application->settings->is_raw_compose_deployment_enabled)
<x-forms.textarea rows="10" readonly id="application.docker_compose_raw" <x-forms.textarea rows="10" readonly id="application.docker_compose_raw"
label="Docker Compose Content (applicationId: {{ $application->id }})" label="Docker Compose Content (applicationId: {{ $application->id }})"

View File

@@ -253,6 +253,11 @@ if [ "$OS_TYPE" = "endeavouros" ]; then
OS_TYPE="arch" OS_TYPE="arch"
fi 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 # Check if the OS is Asahi Linux, if so, change it to fedora
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
OS_TYPE="fedora" OS_TYPE="fedora"
@@ -844,7 +849,7 @@ IPV6_PUBLIC_IP=$(curl -6s https://ifconfig.io || true)
echo -e "\nYour instance is ready to use!\n" echo -e "\nYour instance is ready to use!\n"
if [ -n "$IPV4_PUBLIC_IP" ]; then 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 fi
if [ -n "$IPV6_PUBLIC_IP" ]; then if [ -n "$IPV6_PUBLIC_IP" ]; then
echo -e "You can access Coolify through your Public IPv6: http://[$IPV6_PUBLIC_IP]:8000" echo -e "You can access Coolify through your Public IPv6: http://[$IPV6_PUBLIC_IP]:8000"

View File

@@ -6,7 +6,7 @@
services: services:
postiz: postiz:
image: ghcr.io/gitroomhq/postiz-app:latest image: ghcr.io/gitroomhq/postiz-app:v1.60.1
environment: environment:
- SERVICE_FQDN_POSTIZ_5000 - SERVICE_FQDN_POSTIZ_5000
- MAIN_URL=${SERVICE_FQDN_POSTIZ} - MAIN_URL=${SERVICE_FQDN_POSTIZ}

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.420.3" "version": "4.0.0-beta.420.4"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.420.4" "version": "4.0.0-beta.420.5"
}, },
"helper": { "helper": {
"version": "1.0.8" "version": "1.0.8"