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) {
$internalPort = match ($databaseType) {
'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
{
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;
}
}

View File

@@ -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();

View File

@@ -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;
@@ -471,7 +472,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 +481,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 +888,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 +896,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) {
@@ -908,17 +913,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') {
@@ -930,20 +925,28 @@ 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 = 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 {
$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') {
@@ -956,6 +959,23 @@ 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 = 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()) {
$this->env_filename = null;
@@ -1367,9 +1387,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} ";
$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={$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)) {
$this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";
@@ -1715,10 +1742,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();
@@ -2253,9 +2276,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]

View File

@@ -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.');
}

View File

@@ -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'],
],
]);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
);
}

View File

@@ -1260,26 +1260,17 @@ 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) {
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);
}

View File

@@ -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]);
@@ -2931,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', [])),
@@ -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
if ($key->startsWith('SERVICE_FQDN_')) {
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
@@ -3134,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('_');
@@ -3149,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('_');
@@ -3599,7 +3618,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;
}
@@ -3637,9 +3657,30 @@ 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 = 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(',');
if ($isPullRequest) {
$preview = $resource->previews()->find($preview_id);
@@ -3671,7 +3712,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}
}
}
$defaultLabels = defaultLabels(
id: $resource->id,
name: $containerName,
@@ -3681,6 +3721,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);
@@ -3702,10 +3743,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(','));
}

View File

@@ -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),

View File

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

View File

@@ -69,7 +69,7 @@
<x-forms.button wire:click="generateNginxConfiguration">Generate Default Nginx
Configuration</x-forms.button>
@endif
<div class="w-96 pb-8">
<div class="w-96 pb-6">
@if ($application->could_set_build_commands())
<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." />
@@ -176,7 +176,7 @@
@endif
</div>
@endif
<div class="py-4 border-b dark:border-coolgray-200">
<div>
<h3>Build</h3>
@if ($application->build_pack === 'dockerimage')
<x-forms.input
@@ -290,8 +290,11 @@
@endif
</div>
@if ($application->build_pack === 'dockercompose')
<x-forms.button wire:target='initLoadingCompose'
x-on:click="$wire.dispatch('loadCompose', false)">Reload Compose File</x-forms.button>
<div class="flex items-center gap-2 pb-4">
<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)
<x-forms.textarea rows="10" readonly id="application.docker_compose_raw"
label="Docker Compose Content (applicationId: {{ $application->id }})"

View File

@@ -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"
@@ -844,7 +849,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"

View File

@@ -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}

File diff suppressed because one or more lines are too long

View File

@@ -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"