backupDatabase(); Sleep::for(10)->seconds(); return; } $settings = instanceSettings(); $this->server = Server::find(0); if (! $this->server) { return; } // Fetch fresh version from CDN instead of using cache try { // A manual update runs in the web request, so it does not wait for a retry; it falls back to the cache. $response = cdn_http(tries: $manual_update ? 1 : 3) ->get(config('constants.coolify.versions_url')); if ($response->successful()) { $versions = $response->json(); $this->latestVersion = data_get($versions, 'coolify.v4.version'); } else { // Fallback to cache if CDN unavailable $cacheVersion = get_latest_version_of_coolify(); // Validate cache version against current running version if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) { Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ 'cached_version' => $cacheVersion, 'current_version' => config('constants.coolify.version'), ]); throw new \Exception( 'Cannot determine latest version: CDN unavailable and cache version '. "({$cacheVersion}) is older than running version (".config('constants.coolify.version').')' ); } $this->latestVersion = $cacheVersion; Log::warning('Failed to fetch fresh version from CDN (unsuccessful response), using validated cache', [ 'version' => $cacheVersion, ]); } } catch (\Throwable $e) { $cacheVersion = get_latest_version_of_coolify(); // Validate cache version against current running version if ($cacheVersion && version_compare($cacheVersion, config('constants.coolify.version'), '<')) { Log::error('Failed to fetch fresh version from CDN and cache is corrupted/outdated', [ 'error' => $e->getMessage(), 'cached_version' => $cacheVersion, 'current_version' => config('constants.coolify.version'), ]); throw new \Exception( 'Cannot determine latest version: CDN unavailable and cache version '. "({$cacheVersion}) is older than running version (".config('constants.coolify.version').')' ); } $this->latestVersion = $cacheVersion; Log::warning('Failed to fetch fresh version from CDN, using validated cache', [ 'error' => $e->getMessage(), 'version' => $cacheVersion, ]); } $this->currentVersion = config('constants.coolify.version'); if (! $manual_update) { if (! $settings->is_auto_update_enabled) { return; } if ($this->latestVersion === $this->currentVersion) { return; } if (version_compare($this->latestVersion, $this->currentVersion, '<')) { return; } } // ALWAYS check for downgrades (even for manual updates) if (version_compare($this->latestVersion, $this->currentVersion, '<')) { Log::error('Downgrade prevented', [ 'target_version' => $this->latestVersion, 'current_version' => $this->currentVersion, 'manual_update' => $manual_update, ]); throw new \Exception( "Cannot downgrade from {$this->currentVersion} to {$this->latestVersion}. ". 'If you need to downgrade, please do so manually via Docker commands.' ); } $this->backupDatabase(); $this->update(); $settings->new_version_available = false; $settings->save(); } private function backupDatabase(): void { $backup = StandalonePostgresql::find(0)?->scheduledBackups()->first(); if (! $backup || ! instanceSettings()->is_backup_before_update_enabled) { return; } // Runs inline so the update waits for the result. dispatchSync() would go through the job's // WithoutOverlapping middleware, which silently drops the run while the scheduled backup is running. $job = new DatabaseBackupJob($backup); $job->handle(); if ($job->backup_log?->status !== 'success') { throw new \Exception('The database backup failed, so the update was not started.'); } } private function update() { $this->ensureNoUpgradeIsRunning(); $latestHelperImageVersion = getHelperVersion(); $upgradeScriptUrl = config('constants.coolify.upgrade_script_url'); $registryUrl = coolifyRegistryUrl(); remote_process([ "curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh", 'bash /data/coolify/source/upgrade.sh '. escapeshellarg($this->latestVersion).' '. escapeshellarg($latestHelperImageVersion).' '. escapeshellarg($registryUrl), ], $this->server); } private function ensureNoUpgradeIsRunning(): void { $status = $this->readUpgradeStatus(); if (CoolifyUpgradeStatus::isRunning((string) $status)) { Log::warning('Upgrade skipped because another upgrade is running', [ 'target_version' => $this->latestVersion, 'status' => $status, ]); throw new \Exception( 'Another Coolify upgrade is already running. Wait for it to finish. '. 'If it has stopped, you can upgrade again '.CoolifyUpgradeStatus::RUNNING_LOCK_EXPIRES_AFTER_MINUTES.' minutes after its last status update.' ); } } protected function readUpgradeStatus(): ?string { return instant_remote_process( ['cat '.CoolifyUpgradeStatus::FILE.' 2>/dev/null || true'], $this->server, false ); } }