From ddcb14500de4cb0533822ac9c4eaef26ca30be0d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:47:54 +0200 Subject: [PATCH] refactor(proxy-status): refactored how the proxy status is handled on the UI and on the backend feat(cloudflare): improved cloudflare tunnel automated installation --- app/Actions/CoolifyTask/RunRemoteProcess.php | 11 +- app/Actions/Proxy/CheckProxy.php | 664 +++++++++++++----- app/Actions/Proxy/StartProxy.php | 9 +- app/Actions/Proxy/StopProxy.php | 3 + app/Actions/Server/ConfigureCloudflared.php | 33 +- app/Events/CloudflareTunnelChanged.php | 14 + app/Events/ProxyStatusChanged.php | 25 +- app/Events/ProxyStatusChangedUI.php | 35 + app/Jobs/PushServerUpdateJob.php | 54 +- app/Jobs/RestartProxyJob.php | 3 +- .../CloudflareTunnelChangedNotification.php | 66 ++ app/Listeners/ProxyStartedNotification.php | 2 + .../ProxyStatusChangedNotification.php | 34 + app/Livewire/Server/CloudflareTunnel.php | 95 +++ app/Livewire/Server/CloudflareTunnels.php | 54 -- .../Server/ConfigureCloudflareTunnels.php | 54 -- app/Livewire/Server/Navbar.php | 119 ++++ app/Livewire/Server/Proxy.php | 10 +- app/Livewire/Server/Proxy/Deploy.php | 106 --- .../Server/Proxy/DynamicConfigurations.php | 2 +- app/Livewire/Server/Proxy/Show.php | 10 +- app/Livewire/Server/Proxy/Status.php | 77 -- app/Livewire/Server/Show.php | 4 - app/Models/Server.php | 5 + app/Providers/EventServiceProvider.php | 7 +- bootstrap/helpers/proxy.php | 4 - ...06_06_073345_create_server_previous_ip.php | 28 + .../views/components/server/navbar.blade.php | 60 -- .../views/components/server/sidebar.blade.php | 6 +- .../execute-container-command.blade.php | 2 +- .../views/livewire/server/advanced.blade.php | 2 +- .../server/ca-certificate/show.blade.php | 4 +- .../views/livewire/server/charts.blade.php | 2 +- .../server/cloudflare-tunnel.blade.php | 122 ++++ .../server/cloudflare-tunnels.blade.php | 53 -- .../configure-cloudflare-tunnels.blade.php | 6 - .../views/livewire/server/delete.blade.php | 2 +- .../livewire/server/destinations.blade.php | 2 +- .../livewire/server/docker-cleanup.blade.php | 9 +- .../livewire/server/log-drains.blade.php | 2 +- .../views/livewire/server/navbar.blade.php | 178 +++++ .../server/private-key/show.blade.php | 2 +- .../livewire/server/proxy/deploy.blade.php | 93 --- .../proxy/dynamic-configurations.blade.php | 2 +- .../livewire/server/proxy/logs.blade.php | 2 +- .../livewire/server/proxy/show.blade.php | 2 +- .../livewire/server/proxy/status.blade.php | 17 - .../views/livewire/server/resources.blade.php | 2 +- .../server/security/patches.blade.php | 2 +- .../views/livewire/server/show.blade.php | 2 +- routes/web.php | 4 +- 51 files changed, 1277 insertions(+), 829 deletions(-) create mode 100644 app/Events/CloudflareTunnelChanged.php create mode 100644 app/Events/ProxyStatusChangedUI.php create mode 100644 app/Listeners/CloudflareTunnelChangedNotification.php create mode 100644 app/Listeners/ProxyStatusChangedNotification.php create mode 100644 app/Livewire/Server/CloudflareTunnel.php delete mode 100644 app/Livewire/Server/CloudflareTunnels.php delete mode 100644 app/Livewire/Server/ConfigureCloudflareTunnels.php create mode 100644 app/Livewire/Server/Navbar.php delete mode 100644 app/Livewire/Server/Proxy/Deploy.php delete mode 100644 app/Livewire/Server/Proxy/Status.php create mode 100644 database/migrations/2025_06_06_073345_create_server_previous_ip.php delete mode 100644 resources/views/components/server/navbar.blade.php create mode 100644 resources/views/livewire/server/cloudflare-tunnel.blade.php delete mode 100644 resources/views/livewire/server/cloudflare-tunnels.blade.php delete mode 100644 resources/views/livewire/server/configure-cloudflare-tunnels.blade.php create mode 100644 resources/views/livewire/server/navbar.blade.php delete mode 100644 resources/views/livewire/server/proxy/deploy.blade.php delete mode 100644 resources/views/livewire/server/proxy/status.blade.php diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index 8ce9aa1f2..e0dacfbec 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -104,14 +104,11 @@ class RunRemoteProcess $this->activity->save(); if ($this->call_event_on_finish) { try { - if ($this->call_event_data) { - event(resolve("App\\Events\\$this->call_event_on_finish", [ - 'data' => $this->call_event_data, - ])); + $eventClass = "App\\Events\\$this->call_event_on_finish"; + if (! is_null($this->call_event_data)) { + event(new $eventClass($this->call_event_data)); } else { - event(resolve("App\\Events\\$this->call_event_on_finish", [ - 'userId' => $this->activity->causer_id, - ])); + event(new $eventClass($this->activity->causer_id)); } } catch (\Throwable $e) { Log::error('Error calling event: '.$e->getMessage()); diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 5a2562073..895bed410 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -3,6 +3,7 @@ namespace App\Actions\Proxy; use App\Enums\ProxyTypes; +use App\Events\ProxyStatusChanged; use App\Models\Server; use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; @@ -12,12 +13,38 @@ class CheckProxy { use AsAction; - // It should return if the proxy should be started (true) or not (false) - public function handle(Server $server, $fromUI = false): bool + /** + * Determine if the proxy should be started + * + * @param bool $fromUI Whether this check is initiated from the UI + * @return bool True if proxy should be started, false otherwise + */ + public function handle(Server $server, bool $fromUI = false): bool { + // Early validation checks + if (! $this->shouldProxyRun($server, $fromUI)) { + return false; + } + + // Handle swarm vs standalone differently + if ($server->isSwarm()) { + return $this->checkSwarmProxy($server); + } + + return $this->checkStandaloneProxy($server, $fromUI); + } + + /** + * Basic validation to determine if proxy can run at all + */ + private function shouldProxyRun(Server $server, bool $fromUI): bool + { + // Server must be functional if (! $server->isFunctional()) { return false; } + + // Build servers don't need proxy if ($server->isBuildServer()) { if ($server->proxy) { $server->proxy = null; @@ -26,86 +53,308 @@ class CheckProxy return false; } + $proxyType = $server->proxyType(); + + // Check if proxy is disabled or force stopped if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) && ! $fromUI) { return false; } + + // Validate proxy configuration if (! $server->isProxyShouldRun()) { if ($fromUI) { throw new \Exception('Proxy should not run. You selected the Custom Proxy.'); - } else { - return false; } + + return false; } - // Determine proxy container name based on environment - $proxyContainerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; + return true; + } - if ($server->isSwarm()) { - $status = getContainerStatus($server, $proxyContainerName); + /** + * Check proxy status for swarm mode + */ + private function checkSwarmProxy(Server $server): bool + { + $proxyContainerName = 'coolify-proxy_traefik'; + $status = getContainerStatus($server, $proxyContainerName); + + // Update status if changed + if ($server->proxy->status !== $status) { $server->proxy->set('status', $status); $server->save(); - if ($status === 'running') { - return false; - } - - return true; - } else { - $status = getContainerStatus($server, $proxyContainerName); - if ($status === 'running') { - $server->proxy->set('status', 'running'); - $server->save(); - - return false; - } - if ($server->settings->is_cloudflare_tunnel) { - return false; - } - $ip = $server->ip; - if ($server->id === 0) { - $ip = 'host.docker.internal'; - } - $portsToCheck = ['80', '443']; - - foreach ($portsToCheck as $port) { - // Use the smart port checker that handles dual-stack properly - if ($this->isPortConflict($server, $port, $proxyContainerName)) { - if ($fromUI) { - throw new \Exception("Port $port is in use.
You must stop the process using this port.

Docs: https://coolify.io/docs
Discord: https://coolify.io/discord"); - } else { - return false; - } - } - } - try { - if ($server->proxyType() !== ProxyTypes::NONE->value) { - $proxyCompose = CheckConfiguration::run($server); - if (isset($proxyCompose)) { - $yaml = Yaml::parse($proxyCompose); - $portsToCheck = []; - if ($server->proxyType() === ProxyTypes::TRAEFIK->value) { - $ports = data_get($yaml, 'services.traefik.ports'); - } elseif ($server->proxyType() === ProxyTypes::CADDY->value) { - $ports = data_get($yaml, 'services.caddy.ports'); - } - if (isset($ports)) { - foreach ($ports as $port) { - $portsToCheck[] = str($port)->before(':')->value(); - } - } - } - } else { - $portsToCheck = []; - } - } catch (\Exception $e) { - Log::error('Error checking proxy: '.$e->getMessage()); - } - if (count($portsToCheck) === 0) { - return false; - } - - return true; } + + // If running, no need to start + return $status !== 'running'; + } + + /** + * Check proxy status for standalone mode + */ + private function checkStandaloneProxy(Server $server, bool $fromUI): bool + { + $proxyContainerName = 'coolify-proxy'; + $status = getContainerStatus($server, $proxyContainerName); + + if ($server->proxy->status !== $status) { + $server->proxy->set('status', $status); + $server->save(); + ProxyStatusChanged::dispatch($server->id); + } + + // If already running, no need to start + if ($status === 'running') { + return false; + } + + // Cloudflare tunnel doesn't need proxy + if ($server->settings->is_cloudflare_tunnel) { + return false; + } + + // Check for port conflicts + $portsToCheck = $this->getPortsToCheck($server); + + if (empty($portsToCheck)) { + return false; + } + + $this->validatePortsAvailable($server, $portsToCheck, $proxyContainerName, $fromUI); + + return true; + } + + /** + * Get list of ports that need to be available for the proxy + */ + private function getPortsToCheck(Server $server): array + { + $defaultPorts = ['80', '443']; + + try { + if ($server->proxyType() === ProxyTypes::NONE->value) { + return []; + } + + $proxyCompose = CheckConfiguration::run($server); + if (! $proxyCompose) { + return $defaultPorts; + } + + $yaml = Yaml::parse($proxyCompose); + $ports = $this->extractPortsFromCompose($yaml, $server->proxyType()); + + return ! empty($ports) ? $ports : $defaultPorts; + + } catch (\Exception $e) { + Log::error('Error checking proxy configuration: '.$e->getMessage()); + + return $defaultPorts; + } + } + + /** + * Extract ports from docker-compose configuration + */ + private function extractPortsFromCompose(array $yaml, string $proxyType): array + { + $ports = []; + $servicePorts = null; + + if ($proxyType === ProxyTypes::TRAEFIK->value) { + $servicePorts = data_get($yaml, 'services.traefik.ports'); + } elseif ($proxyType === ProxyTypes::CADDY->value) { + $servicePorts = data_get($yaml, 'services.caddy.ports'); + } + + if ($servicePorts) { + foreach ($servicePorts as $port) { + $ports[] = str($port)->before(':')->value(); + } + } + + return array_unique($ports); + } + + /** + * Validate that required ports are available (checked in parallel) + */ + private function validatePortsAvailable(Server $server, array $ports, string $proxyContainerName, bool $fromUI): void + { + if (empty($ports)) { + return; + } + + \Log::info('Checking ports in parallel: '.implode(', ', $ports)); + + // Check all ports in parallel + $conflicts = $this->checkPortsInParallel($server, $ports, $proxyContainerName); + + // Handle any conflicts found + foreach ($conflicts as $port) { + $message = "Port $port is in use"; + if ($fromUI) { + $message = "Port $port is in use.
You must stop the process using this port.

". + "Docs: https://coolify.io/docs
". + "Discord: https://coolify.io/discord"; + } + throw new \Exception($message); + } + } + + /** + * Check multiple ports for conflicts in parallel using concurrent processes + */ + private function checkPortsInParallel(Server $server, array $ports, string $proxyContainerName): array + { + $conflicts = []; + + // First, quickly filter out ports used by our own proxy + $ourProxyPorts = $this->getOurProxyPorts($server, $proxyContainerName); + $portsToCheck = array_diff($ports, $ourProxyPorts); + + if (empty($portsToCheck)) { + \Log::info('All ports are used by our proxy, no conflicts to check'); + + return []; + } + + // Build a single optimized command to check all ports at once + $command = $this->buildBatchPortCheckCommand($portsToCheck, $server); + if (! $command) { + \Log::warning('No suitable port checking method available, falling back to sequential'); + + return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName); + } + + try { + $output = instant_remote_process([$command], $server, true); + $results = $this->parseBatchPortCheckOutput($output, $portsToCheck); + + foreach ($results as $port => $isConflict) { + if ($isConflict) { + $conflicts[] = $port; + } + } + } catch (\Throwable $e) { + \Log::warning('Batch port check failed, falling back to sequential: '.$e->getMessage()); + + return $this->checkPortsSequentially($server, $portsToCheck, $proxyContainerName); + } + + return $conflicts; + } + + /** + * Get ports currently used by our proxy container + */ + private function getOurProxyPorts(Server $server, string $proxyContainerName): array + { + try { + $getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'"; + $containerId = trim(instant_remote_process([$getProxyContainerId], $server)); + + if (empty($containerId)) { + return []; + } + + $getPortsCommand = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' 2>/dev/null || echo '{}'"; + $portsJson = instant_remote_process([$getPortsCommand], $server, true); + $portsData = json_decode($portsJson, true); + + $ports = []; + if (is_array($portsData)) { + foreach (array_keys($portsData) as $portSpec) { + if (preg_match('/^(\d+)\/tcp$/', $portSpec, $matches)) { + $ports[] = $matches[1]; + } + } + } + + return $ports; + } catch (\Throwable $e) { + \Log::warning('Failed to get proxy ports: '.$e->getMessage()); + + return []; + } + } + + /** + * Build a single command to check multiple ports efficiently + */ + private function buildBatchPortCheckCommand(array $ports, Server $server): ?string + { + $portList = implode('|', $ports); + + // Try different commands in order of preference + $commands = [ + // ss - fastest and most reliable + [ + 'check' => 'command -v ss >/dev/null 2>&1', + 'exec' => "ss -Htuln state listening | grep -E ':($portList) ' | awk '{print \$5}' | cut -d: -f2 | sort -u", + ], + // netstat - widely available + [ + 'check' => 'command -v netstat >/dev/null 2>&1', + 'exec' => "netstat -tuln | grep -E ':($portList) ' | grep LISTEN | awk '{print \$4}' | cut -d: -f2 | sort -u", + ], + // lsof - last resort + [ + 'check' => 'command -v lsof >/dev/null 2>&1', + 'exec' => "lsof -iTCP -sTCP:LISTEN -P -n | grep -E ':($portList) ' | awk '{print \$9}' | cut -d: -f2 | sort -u", + ], + ]; + + foreach ($commands as $cmd) { + try { + instant_remote_process([$cmd['check']], $server); + + return $cmd['exec']; + } catch (\Throwable $e) { + continue; + } + } + + return null; + } + + /** + * Parse output from batch port check command + */ + private function parseBatchPortCheckOutput(string $output, array $portsToCheck): array + { + $results = []; + $usedPorts = array_filter(explode("\n", trim($output))); + + foreach ($portsToCheck as $port) { + $results[$port] = in_array($port, $usedPorts); + } + + return $results; + } + + /** + * Fallback to sequential checking if parallel fails + */ + private function checkPortsSequentially(Server $server, array $ports, string $proxyContainerName): array + { + $conflicts = []; + + foreach ($ports as $port) { + try { + if ($this->isPortConflict($server, $port, $proxyContainerName)) { + $conflicts[] = $port; + } + } catch (\Throwable $e) { + \Log::warning("Sequential port check failed for port $port: ".$e->getMessage()); + // Continue checking other ports + } + } + + return $conflicts; } /** @@ -114,137 +363,168 @@ class CheckProxy */ private function isPortConflict(Server $server, string $port, string $proxyContainerName): bool { - // First check if our own proxy is using this port (which is fine) + // Check if our own proxy is using this port (which is fine) + if ($this->isOurProxyUsingPort($server, $port, $proxyContainerName)) { + return false; + } + + // Try different methods to detect port conflicts + $portCheckMethods = [ + 'checkWithSs', + 'checkWithNetstat', + 'checkWithLsof', + 'checkWithNetcat', + ]; + + foreach ($portCheckMethods as $method) { + try { + $result = $this->$method($server, $port); + if ($result !== null) { + return $result; + } + } catch (\Throwable $e) { + continue; // Try next method + } + } + + // If all methods fail, assume port is free to avoid false positives + return false; + } + + /** + * Check if our own proxy container is using the port + */ + private function isOurProxyUsingPort(Server $server, string $port, string $proxyContainerName): bool + { try { $getProxyContainerId = "docker ps -a --filter name=$proxyContainerName --format '{{.ID}}'"; $containerId = trim(instant_remote_process([$getProxyContainerId], $server)); - if (! empty($containerId)) { - $checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'"; - try { - instant_remote_process([$checkProxyPort], $server); - - // Our proxy is using the port, which is fine - return false; - } catch (\Throwable $e) { - // Our container exists but not using this port - } + if (empty($containerId)) { + return false; } + + $checkProxyPort = "docker inspect $containerId --format '{{json .NetworkSettings.Ports}}' | grep '\"$port/tcp\"'"; + instant_remote_process([$checkProxyPort], $server); + + return true; // Our proxy is using the port } catch (\Throwable $e) { - // Container not found or error checking, continue with regular checks - } - - // Command sets for different ways to check ports, ordered by preference - $commandSets = [ - // Set 1: Use ss to check listener counts by protocol stack - [ - 'available' => 'command -v ss >/dev/null 2>&1', - 'check' => [ - // Get listening process details - "ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"", - // Count IPv4 listeners - "echo \"\$ss_output\" | grep -c ':$port '", - ], - ], - // Set 2: Use netstat as alternative to ss - [ - 'available' => 'command -v netstat >/dev/null 2>&1', - 'check' => [ - // Get listening process details - "netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '", - // Count listeners - "echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'", - ], - ], - // Set 3: Use lsof as last resort - [ - 'available' => 'command -v lsof >/dev/null 2>&1', - 'check' => [ - // Get process using the port - "lsof -i :$port -P -n | grep 'LISTEN'", - // Count listeners - "lsof -i :$port -P -n | grep 'LISTEN' | wc -l", - ], - ], - ]; - - // Try each command set until we find one available - foreach ($commandSets as $set) { - try { - // Check if the command is available - instant_remote_process([$set['available']], $server); - - // Run the actual check commands - $output = instant_remote_process($set['check'], $server, true); - - // Parse the output lines - $lines = explode("\n", trim($output)); - - // Get the detailed output and listener count - $details = trim($lines[0] ?? ''); - $count = intval(trim($lines[1] ?? '0')); - - // If no listeners or empty result, port is free - if ($count == 0 || empty($details)) { - return false; - } - - // Try to detect if this is our coolify-proxy - if (strpos($details, 'docker') !== false || strpos($details, $proxyContainerName) !== false) { - // It's likely our docker or proxy, which is fine - return false; - } - - // Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6) - // If exactly 2 listeners and both have same port, likely dual-stack - if ($count <= 2) { - // Check if it looks like a standard dual-stack setup - $isDualStack = false; - - // Look for IPv4 and IPv6 in the listing (ss output format) - if (preg_match('/LISTEN.*:'.$port.'\s/', $details) && - (preg_match('/\*:'.$port.'\s/', $details) || - preg_match('/:::'.$port.'\s/', $details))) { - $isDualStack = true; - } - - // For netstat format - if (strpos($details, '0.0.0.0:'.$port) !== false && - strpos($details, ':::'.$port) !== false) { - $isDualStack = true; - } - - // For lsof format (IPv4 and IPv6) - if (strpos($details, '*:'.$port) !== false && - preg_match('/\*:'.$port.'.*IPv4/', $details) && - preg_match('/\*:'.$port.'.*IPv6/', $details)) { - $isDualStack = true; - } - - if ($isDualStack) { - return false; // This is just a normal dual-stack setup - } - } - - // If we get here, it's likely a real port conflict - return true; - - } catch (\Throwable $e) { - // This command set failed, try the next one - continue; - } - } - - // Fallback to simpler check if all above methods fail - try { - // Just try to bind to the port directly to see if it's available - $checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'"; - $result = instant_remote_process([$checkCommand], $server, true); - - return trim($result) === 'in-use'; - } catch (\Throwable $e) { - // If everything fails, assume the port is free to avoid false positives return false; } } + + /** + * Check port usage with ss command + */ + private function checkWithSs(Server $server, string $port): ?bool + { + $commands = [ + 'command -v ss >/dev/null 2>&1', + "ss_output=\$(ss -Htuln state listening sport = :$port 2>/dev/null) && echo \"\$ss_output\"", + "echo \"\$ss_output\" | grep -c ':$port '", + ]; + + $output = instant_remote_process($commands, $server, true); + + return $this->parsePortCheckOutput($output, $port); + } + + /** + * Check port usage with netstat command + */ + private function checkWithNetstat(Server $server, string $port): ?bool + { + $commands = [ + 'command -v netstat >/dev/null 2>&1', + "netstat_output=\$(netstat -tuln 2>/dev/null) && echo \"\$netstat_output\" | grep ':$port '", + "echo \"\$netstat_output\" | grep ':$port ' | grep -c 'LISTEN'", + ]; + + $output = instant_remote_process($commands, $server, true); + + return $this->parsePortCheckOutput($output, $port); + } + + /** + * Check port usage with lsof command + */ + private function checkWithLsof(Server $server, string $port): ?bool + { + $commands = [ + 'command -v lsof >/dev/null 2>&1', + "lsof -i :$port -P -n | grep 'LISTEN'", + "lsof -i :$port -P -n | grep 'LISTEN' | wc -l", + ]; + + $output = instant_remote_process($commands, $server, true); + + return $this->parsePortCheckOutput($output, $port); + } + + /** + * Check port usage with netcat as fallback + */ + private function checkWithNetcat(Server $server, string $port): ?bool + { + $checkCommand = "nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free'"; + $result = instant_remote_process([$checkCommand], $server, true); + + return trim($result) === 'in-use'; + } + + /** + * Parse output from port checking commands + */ + private function parsePortCheckOutput(string $output, string $port): ?bool + { + $lines = explode("\n", trim($output)); + $details = trim($lines[0] ?? ''); + $count = intval(trim($lines[1] ?? '0')); + + // No listeners found + if ($count === 0 || empty($details)) { + return false; + } + + // Check if it's likely our docker/proxy process + if (strpos($details, 'docker') !== false || strpos($details, 'coolify-proxy') !== false) { + return false; + } + + // Handle dual-stack scenarios (1-2 listeners for IPv4+IPv6) + if ($count <= 2 && $this->isDualStackSetup($details, $port)) { + return false; + } + + // Real port conflict detected + return true; + } + + /** + * Detect if this is a normal dual-stack setup (IPv4 + IPv6) + */ + private function isDualStackSetup(string $details, string $port): bool + { + $patterns = [ + // ss output format + '/LISTEN.*:'.$port.'\s/', + '/\*:'.$port.'\s/', + '/:::'.$port.'\s/', + // netstat format + '/0\.0\.0\.0:'.$port.'/', + '/:::'.$port.'/', + // lsof format + '/\*:'.$port.'.*IPv[46]/', + ]; + + $matches = 0; + foreach ($patterns as $pattern) { + if (preg_match($pattern, $details)) { + $matches++; + } + } + + // If we see patterns indicating both IPv4 and IPv6, it's likely dual-stack + return $matches >= 2; + } } diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 1fff55861..e685adbdf 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -3,7 +3,7 @@ namespace App\Actions\Proxy; use App\Enums\ProxyTypes; -use App\Events\ProxyStarted; +use App\Events\ProxyStatusChanged; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; use Spatie\Activitylog\Models\Activity; @@ -57,20 +57,19 @@ class StartProxy " echo 'Successfully stopped and removed existing coolify-proxy.'", 'fi', "echo 'Starting coolify-proxy.'", - 'docker compose up -d', + 'docker compose up -d --wait', "echo 'Successfully started coolify-proxy.'", ]); $commands = $commands->merge(connectProxyToNetworks($server)); } if ($async) { - return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); + return remote_process($commands, $server, callEventOnFinish: 'ProxyStatusChanged', callEventData: $server->id); } else { instant_remote_process($commands, $server); - $server->proxy->set('status', 'running'); $server->proxy->set('type', $proxyType); $server->save(); - ProxyStarted::dispatch($server); + ProxyStatusChanged::dispatch($server->id); return 'OK'; } diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php index 75b22d236..7072c846f 100644 --- a/app/Actions/Proxy/StopProxy.php +++ b/app/Actions/Proxy/StopProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Proxy; +use App\Events\ProxyStatusChanged; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; @@ -24,6 +25,8 @@ class StopProxy $server->save(); } catch (\Throwable $e) { return handleError($e); + } finally { + ProxyStatusChanged::dispatch($server->id); } } } diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index fc04e67a4..d21622bc5 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -2,16 +2,16 @@ namespace App\Actions\Server; -use App\Events\CloudflareTunnelConfigured; use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; +use Spatie\Activitylog\Models\Activity; use Symfony\Component\Yaml\Yaml; class ConfigureCloudflared { use AsAction; - public function handle(Server $server, string $cloudflare_token) + public function handle(Server $server, string $cloudflare_token, string $ssh_domain): Activity { try { $config = [ @@ -24,6 +24,13 @@ class ConfigureCloudflared 'command' => 'tunnel run', 'environment' => [ "TUNNEL_TOKEN={$cloudflare_token}", + 'TUNNEL_METRICS=127.0.0.1:60123', + ], + 'healthcheck' => [ + 'test' => ['CMD', 'cloudflared', 'tunnel', '--metrics', '127.0.0.1:60123', 'ready'], + 'interval' => '5s', + 'timeout' => '30s', + 'retries' => 5, ], ], ], @@ -34,22 +41,20 @@ class ConfigureCloudflared 'mkdir -p /tmp/cloudflared', 'cd /tmp/cloudflared', "echo '$docker_compose_yml_base64' | base64 -d | tee docker-compose.yml > /dev/null", + 'echo Pulling latest Cloudflare Tunnel image.', 'docker compose pull', - 'docker compose down -v --remove-orphans > /dev/null 2>&1', - 'docker compose up -d --remove-orphans', + 'echo Stopping existing Cloudflare Tunnel container.', + 'docker rm -f coolify-cloudflared || true', + 'echo Starting new Cloudflare Tunnel container.', + 'docker compose up --wait --wait-timeout 15 --remove-orphans || docker logs coolify-cloudflared', ]); - instant_remote_process($commands, $server); - } catch (\Throwable $e) { - $server->settings->is_cloudflare_tunnel = false; - $server->settings->save(); - throw $e; - } finally { - CloudflareTunnelConfigured::dispatch($server->team_id); - $commands = collect([ - 'rm -fr /tmp/cloudflared', + return remote_process($commands, $server, callEventOnFinish: 'CloudflareTunnelChanged', callEventData: [ + 'server_id' => $server->id, + 'ssh_domain' => $ssh_domain, ]); - instant_remote_process($commands, $server); + } catch (\Throwable $e) { + throw $e; } } } diff --git a/app/Events/CloudflareTunnelChanged.php b/app/Events/CloudflareTunnelChanged.php new file mode 100644 index 000000000..afa848cc2 --- /dev/null +++ b/app/Events/CloudflareTunnelChanged.php @@ -0,0 +1,14 @@ +check() && auth()->user()->currentTeam()) { - $teamId = auth()->user()->currentTeam()->id; - } - $this->teamId = $teamId; - } - - public function broadcastOn(): array - { - if (is_null($this->teamId)) { - return []; - } - - return [ - new PrivateChannel("team.{$this->teamId}"), - ]; - } + public function __construct(public $data) {} } diff --git a/app/Events/ProxyStatusChangedUI.php b/app/Events/ProxyStatusChangedUI.php new file mode 100644 index 000000000..d281972bc --- /dev/null +++ b/app/Events/ProxyStatusChangedUI.php @@ -0,0 +1,35 @@ +check() && auth()->user()->currentTeam()) { + $teamId = auth()->user()->currentTeam()->id; + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + if (is_null($this->teamId)) { + return []; + } + + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php index dc1d4f0f5..9936c76a2 100644 --- a/app/Jobs/PushServerUpdateJob.php +++ b/app/Jobs/PushServerUpdateJob.php @@ -213,8 +213,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue if (! $application) { return; } - $application->status = $containerStatus; - $application->save(); + if ($application->status !== $containerStatus) { + $application->status = $containerStatus; + $application->save(); + } } private function updateApplicationPreviewStatus(string $applicationId, string $pullRequestId, string $containerStatus) @@ -249,8 +251,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue return; } - $application->status = 'exited'; - $application->save(); + if ($application->status !== 'exited') { + $application->status = 'exited'; + $application->save(); + } } }); } @@ -286,8 +290,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue return; } - $applicationPreview->status = 'exited'; - $applicationPreview->save(); + if ($applicationPreview->status !== 'exited') { + $applicationPreview->status = 'exited'; + $applicationPreview->save(); + } } }); } @@ -318,8 +324,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue if (! $database) { return; } - $database->status = $containerStatus; - $database->save(); + if ($database->status !== $containerStatus) { + $database->status = $containerStatus; + $database->save(); + } if ($this->isRunning($containerStatus) && $tcpProxy) { $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) { return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running'; @@ -339,8 +347,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue $notFoundDatabaseUuids->each(function ($databaseUuid) { $database = $this->databases->where('uuid', $databaseUuid)->first(); if ($database) { - $database->status = 'exited'; - $database->save(); + if ($database->status !== 'exited') { + $database->status = 'exited'; + $database->save(); + } if ($database->is_public) { StopDatabaseProxy::dispatch($database); } @@ -358,14 +368,18 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue if ($subType === 'application') { $application = $service->applications()->where('id', $subId)->first(); if ($application) { - $application->status = $containerStatus; - $application->save(); + if ($application->status !== $containerStatus) { + $application->status = $containerStatus; + $application->save(); + } } } elseif ($subType === 'database') { $database = $service->databases()->where('id', $subId)->first(); if ($database) { - $database->status = $containerStatus; - $database->save(); + if ($database->status !== $containerStatus) { + $database->status = $containerStatus; + $database->save(); + } } } } @@ -378,8 +392,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue $notFoundServiceApplicationIds->each(function ($serviceApplicationId) { $application = ServiceApplication::find($serviceApplicationId); if ($application) { - $application->status = 'exited'; - $application->save(); + if ($application->status !== 'exited') { + $application->status = 'exited'; + $application->save(); + } } }); } @@ -387,8 +403,10 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { $database = ServiceDatabase::find($serviceDatabaseId); if ($database) { - $database->status = 'exited'; - $database->save(); + if ($database->status !== 'exited') { + $database->status = 'exited'; + $database->save(); + } } }); } diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php index a05652a4c..5e862236b 100644 --- a/app/Jobs/RestartProxyJob.php +++ b/app/Jobs/RestartProxyJob.php @@ -36,9 +36,10 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue $this->server->proxy->force_stop = false; $this->server->save(); + StartProxy::run($this->server, force: true); - CheckProxy::run($this->server, true); + // CheckProxy::run($this->server, true); } catch (\Throwable $e) { return handleError($e); } diff --git a/app/Listeners/CloudflareTunnelChangedNotification.php b/app/Listeners/CloudflareTunnelChangedNotification.php new file mode 100644 index 000000000..8b88b11f5 --- /dev/null +++ b/app/Listeners/CloudflareTunnelChangedNotification.php @@ -0,0 +1,66 @@ +server = Server::find($server_id)->firstOrFail(); + + // Check if cloudflare tunnel is running (container is healthy) - try 3 times with 5 second intervals + $cloudflareHealthy = false; + $attempts = 3; + + for ($i = 1; $i <= $attempts; $i++) { + \Log::debug("Cloudflare health check attempt {$i}/{$attempts}", ['server_id' => $server_id]); + $result = instant_remote_process_with_timeout(['docker inspect coolify-cloudflared | jq -e ".[0].State.Health.Status == \"healthy\""'], $this->server, false, 10); + + if (blank($result)) { + \Log::debug("Cloudflare Tunnels container not found on attempt {$i}", ['server_id' => $server_id]); + } elseif ($result === 'true') { + \Log::debug("Cloudflare Tunnels container healthy on attempt {$i}", ['server_id' => $server_id]); + $cloudflareHealthy = true; + break; + } else { + \Log::debug("Cloudflare Tunnels container not healthy on attempt {$i}", ['server_id' => $server_id, 'result' => $result]); + } + + // Sleep between attempts (except after the last attempt) + if ($i < $attempts) { + Sleep::for(5)->seconds(); + } + } + + if (! $cloudflareHealthy) { + \Log::error('Cloudflare Tunnels container failed all health checks.', ['server_id' => $server_id, 'attempts' => $attempts]); + + return; + } + $this->server->settings->update([ + 'is_cloudflare_tunnel' => true, + ]); + + // Only update IP if it's not already set to the ssh_domain or if it's empty + if ($this->server->ip !== $ssh_domain && ! empty($ssh_domain)) { + \Log::debug('Cloudflare Tunnels configuration updated - updating IP address.', ['old_ip' => $this->server->ip, 'new_ip' => $ssh_domain]); + $this->server->update(['ip' => $ssh_domain]); + } else { + \Log::debug('Cloudflare Tunnels configuration updated - IP address unchanged.', ['current_ip' => $this->server->ip]); + } + $teamId = $this->server->team_id; + CloudflareTunnelConfigured::dispatch($teamId); + } +} diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php index 9045b1e5c..a39bf9253 100644 --- a/app/Listeners/ProxyStartedNotification.php +++ b/app/Listeners/ProxyStartedNotification.php @@ -3,6 +3,7 @@ namespace App\Listeners; use App\Events\ProxyStarted; +use App\Events\ProxyStatusChanged; use App\Models\Server; class ProxyStartedNotification @@ -18,5 +19,6 @@ class ProxyStartedNotification $this->server->setupDynamicProxyConfiguration(); $this->server->proxy->force_stop = false; $this->server->save(); + // ProxyStatusChanged::dispatch( $this->server->id); } } diff --git a/app/Listeners/ProxyStatusChangedNotification.php b/app/Listeners/ProxyStatusChangedNotification.php new file mode 100644 index 000000000..cad8c3c7b --- /dev/null +++ b/app/Listeners/ProxyStatusChangedNotification.php @@ -0,0 +1,34 @@ +data; + $server = Server::find($serverId); + if (is_null($server)) { + return; + } + $proxyContainerName = 'coolify-proxy'; + $status = getContainerStatus($server, $proxyContainerName); + $server->proxy->set('status', $status); + $server->save(); + + if ($status === 'running') { + $server->setupDefaultRedirect(); + $server->setupDynamicProxyConfiguration(); + $server->proxy->force_stop = false; + $server->save(); + } + ProxyStatusChangedUI::dispatch($server->team_id); + } +} diff --git a/app/Livewire/Server/CloudflareTunnel.php b/app/Livewire/Server/CloudflareTunnel.php new file mode 100644 index 000000000..0407aa0e4 --- /dev/null +++ b/app/Livewire/Server/CloudflareTunnel.php @@ -0,0 +1,95 @@ +user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh', + ]; + } + + public function refresh() + { + $this->mount($this->server->uuid); + } + + public function mount(string $server_uuid) + { + try { + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + if ($this->server->isLocalhost()) { + return redirect()->route('server.show', ['server_uuid' => $server_uuid]); + } + $this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel; + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function toggleCloudflareTunnels() + { + try { + remote_process(['docker rm -f coolify-cloudflared'], $this->server, false, 10); + $this->isCloudflareTunnelsEnabled = false; + $this->server->settings->is_cloudflare_tunnel = false; + $this->server->settings->save(); + if ($this->server->ip_previous) { + $this->server->update(['ip' => $this->server->ip_previous]); + $this->dispatch('success', 'Cloudflare Tunnel disabled.

Manually updated the server IP address to its previous IP address.'); + } else { + $this->dispatch('success', 'Cloudflare Tunnel disabled. Please update the server IP address to its real IP address in the server settings.'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCloudflareConfig() + { + $this->isCloudflareTunnelsEnabled = true; + $this->server->settings->is_cloudflare_tunnel = true; + $this->server->settings->save(); + $this->server->refresh(); + $this->dispatch('success', 'Cloudflare Tunnel enabled.'); + } + + public function automatedCloudflareConfig() + { + try { + if (str($this->ssh_domain)->contains('https://')) { + $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim(); + $this->ssh_domain = str($this->ssh_domain)->replace('/', ''); + } + $activity = ConfigureCloudflared::run($this->server, $this->cloudflare_token, $this->ssh_domain); + $this->dispatch('activityMonitor', $activity->id); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.cloudflare-tunnel'); + } +} diff --git a/app/Livewire/Server/CloudflareTunnels.php b/app/Livewire/Server/CloudflareTunnels.php deleted file mode 100644 index f69fc8655..000000000 --- a/app/Livewire/Server/CloudflareTunnels.php +++ /dev/null @@ -1,54 +0,0 @@ -server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); - if ($this->server->isLocalhost()) { - return redirect()->route('server.show', ['server_uuid' => $server_uuid]); - } - $this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel; - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function instantSave() - { - try { - $this->validate(); - $this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled; - $this->server->settings->save(); - $this->dispatch('success', 'Server updated.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function manualCloudflareConfig() - { - $this->isCloudflareTunnelsEnabled = true; - $this->server->settings->is_cloudflare_tunnel = true; - $this->server->settings->save(); - $this->server->refresh(); - $this->dispatch('success', 'Cloudflare Tunnels enabled.'); - } - - public function render() - { - return view('livewire.server.cloudflare-tunnels'); - } -} diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php deleted file mode 100644 index f27614aa4..000000000 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ /dev/null @@ -1,54 +0,0 @@ -where('id', $this->server_id)->firstOrFail(); - $server->settings->is_cloudflare_tunnel = true; - $server->settings->save(); - $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('refreshServerShow'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function submit() - { - try { - if (str($this->ssh_domain)->contains('https://')) { - $this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim(); - // remove / from the end - $this->ssh_domain = str($this->ssh_domain)->replace('/', ''); - } - $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); - ConfigureCloudflared::dispatch($server, $this->cloudflare_token); - $server->settings->is_cloudflare_tunnel = true; - $server->ip = $this->ssh_domain; - $server->save(); - $server->settings->save(); - $this->dispatch('info', 'Cloudflare Tunnels configuration started.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function render() - { - return view('livewire.server.configure-cloudflare-tunnels'); - } -} diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php new file mode 100644 index 000000000..b5e7f928a --- /dev/null +++ b/app/Livewire/Server/Navbar.php @@ -0,0 +1,119 @@ +user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},ProxyStatusChangedUI" => 'showNotification', + ]; + } + + public function mount(Server $server) + { + $this->server = $server; + $this->currentRoute = request()->route()->getName(); + } + + public function restart() + { + try { + RestartProxyJob::dispatch($this->server); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function checkProxy() + { + try { + CheckProxy::run($this->server, true); + $this->dispatch('startProxy')->self(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function startProxy() + { + try { + $activity = StartProxy::run($this->server, force: true); + $this->dispatch('activityMonitor', $activity->id); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function stop(bool $forceStop = true) + { + try { + StopProxy::dispatch($this->server, $forceStop); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function checkProxyStatus() + { + if ($this->isChecking) { + return; + } + + try { + $this->isChecking = true; + CheckProxy::run($this->server, true); + $this->showNotification(); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->isChecking = false; + } + } + + public function showNotification() + { + $status = $this->server->proxy->status ?? 'unknown'; + $forceStop = $this->server->proxy->force_stop ?? false; + + switch ($status) { + case 'running': + $this->dispatch('success', 'Proxy is running.'); + break; + case 'restarting': + $this->dispatch('info', 'Initiating proxy restart.'); + break; + case 'exited': + if ($forceStop) { + $this->dispatch('info', 'Proxy is stopped manually.'); + } else { + $this->dispatch('info', 'Proxy is stopped manually. Starting in a moment.'); + } + break; + default: + $this->dispatch('warning', 'Proxy is not running.'); + break; + } + } + + public function render() + { + return view('livewire.server.navbar'); + } +} diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 4e325c1ff..266e6fd0b 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -19,7 +19,7 @@ class Proxy extends Component public ?string $redirect_url = null; - protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; + protected $listeners = ['saveConfiguration' => 'submit']; protected $rules = [ 'server.settings.generate_exact_labels' => 'required|boolean', @@ -32,10 +32,10 @@ class Proxy extends Component $this->redirect_url = data_get($this->server, 'proxy.redirect_url'); } - public function proxyStatusUpdated() - { - $this->dispatch('refresh')->self(); - } + // public function proxyStatusUpdated() + // { + // $this->dispatch('refresh')->self(); + // } public function changeProxy() { diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php deleted file mode 100644 index 48eede4e5..000000000 --- a/app/Livewire/Server/Proxy/Deploy.php +++ /dev/null @@ -1,106 +0,0 @@ -user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},ProxyStatusChanged" => 'proxyStarted', - 'proxyStatusUpdated', - 'traefikDashboardAvailable', - 'serverRefresh' => 'proxyStatusUpdated', - 'checkProxy', - 'startProxy', - 'proxyChanged' => 'proxyStatusUpdated', - ]; - } - - public function mount() - { - if ($this->server->id === 0) { - $this->serverIp = base_ip(); - } else { - $this->serverIp = $this->server->ip; - } - $this->currentRoute = request()->route()->getName(); - } - - public function traefikDashboardAvailable(bool $data) - { - $this->traefikDashboardAvailable = $data; - } - - public function proxyStarted() - { - CheckProxy::run($this->server, true); - $this->dispatch('proxyStatusUpdated'); - } - - public function proxyStatusUpdated() - { - $this->server->refresh(); - } - - public function restart() - { - try { - RestartProxyJob::dispatch($this->server); - $this->dispatch('checkProxy'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function checkProxy() - { - try { - CheckProxy::run($this->server, true); - $this->dispatch('startProxyPolling'); - $this->dispatch('proxyChecked'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function startProxy() - { - try { - $this->server->proxy->force_stop = false; - $this->server->save(); - $activity = StartProxy::run($this->server, force: true); - $this->dispatch('activityMonitor', $activity->id, ProxyStatusChanged::class); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function stop(bool $forceStop = true) - { - try { - StopProxy::run($this->server, $forceStop); - $this->dispatch('proxyStatusUpdated'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index 7db890638..bb9d0f673 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -19,7 +19,7 @@ class DynamicConfigurations extends Component $teamId = auth()->user()->currentTeam()->id; return [ - "echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations', + "echo-private:team.{$teamId},ProxyStatusChangedUI" => 'loadDynamicConfigurations', 'loadDynamicConfigurations', ]; } diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php index 5ecb56a69..06b51eee5 100644 --- a/app/Livewire/Server/Proxy/Show.php +++ b/app/Livewire/Server/Proxy/Show.php @@ -11,12 +11,12 @@ class Show extends Component public $parameters = []; - protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated']; + // protected $listeners = ['proxyStatusUpdated']; - public function proxyStatusUpdated() - { - $this->server->refresh(); - } + // public function proxyStatusUpdated() + // { + // $this->server->refresh(); + // } public function mount() { diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php deleted file mode 100644 index f4f18381f..000000000 --- a/app/Livewire/Server/Proxy/Status.php +++ /dev/null @@ -1,77 +0,0 @@ -checkProxy(); - } - - public function proxyStatusUpdated() - { - $this->server->refresh(); - } - - public function checkProxy(bool $notification = false) - { - try { - if ($this->polling) { - if ($this->numberOfPolls >= 10) { - $this->polling = false; - $this->numberOfPolls = 0; - $notification && $this->dispatch('error', 'Proxy is not running.'); - - return; - } - $this->numberOfPolls++; - } - $shouldStart = CheckProxy::run($this->server, true); - if ($shouldStart) { - StartProxy::run($this->server, false); - } - $this->dispatch('proxyStatusUpdated'); - if ($this->server->proxy->status === 'running') { - $this->polling = false; - $notification && $this->dispatch('success', 'Proxy is running.'); - } elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) { - $notification && $this->dispatch('error', 'Proxy has exited.'); - } elseif ($this->server->proxy->force_stop) { - $notification && $this->dispatch('error', 'Proxy is stopped manually.'); - } else { - $notification && $this->dispatch('error', 'Proxy is not running.'); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function getProxyStatus() - { - try { - GetContainersStatus::run($this->server); - // dispatch_sync(new ContainerStatusJob($this->server)); - $this->dispatch('proxyStatusUpdated'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index b0e6d8858..1d37a9c8c 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -86,10 +86,7 @@ class Show extends Component public function getListeners() { - $teamId = auth()->user()->currentTeam()->id; - return [ - "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh', 'refreshServerShow' => 'refresh', ]; } @@ -211,7 +208,6 @@ class Show extends Component $this->server->settings->is_usable = $this->isUsable = true; $this->server->settings->save(); ServerReachabilityChanged::dispatch($this->server); - $this->dispatch('proxyStatusUpdated'); } else { $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); diff --git a/app/Models/Server.php b/app/Models/Server.php index 7e0a10ce8..eac5bbcc1 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -69,6 +69,11 @@ class Server extends BaseModel } if ($server->ip) { $payload['ip'] = str($server->ip)->trim(); + + // Update ip_previous when ip is being changed + if ($server->isDirty('ip') && $server->getOriginal('ip')) { + $payload['ip_previous'] = $server->getOriginal('ip'); + } } $server->forceFill($payload); }); diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index d76ec3037..b960dd8e3 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,10 +2,8 @@ namespace App\Providers; -use App\Events\ProxyStarted; use App\Listeners\MaintenanceModeDisabledNotification; use App\Listeners\MaintenanceModeEnabledNotification; -use App\Listeners\ProxyStartedNotification; use Illuminate\Foundation\Events\MaintenanceModeDisabled; use Illuminate\Foundation\Events\MaintenanceModeEnabled; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -30,9 +28,6 @@ class EventServiceProvider extends ServiceProvider GoogleExtendSocialite::class.'@handle', InfomaniakExtendSocialite::class.'@handle', ], - ProxyStarted::class => [ - ProxyStartedNotification::class, - ], ]; public function boot(): void @@ -42,6 +37,6 @@ class EventServiceProvider extends ServiceProvider public function shouldDiscoverEvents(): bool { - return false; + return true; } } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 714358e37..cabdabaa7 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -91,21 +91,17 @@ function connectProxyToNetworks(Server $server) if ($server->isSwarm()) { $commands = $networks->map(function ($network) { return [ - "echo 'Connecting coolify-proxy to $network network...'", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --driver overlay --attachable $network >/dev/null", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", "echo 'Successfully connected coolify-proxy to $network network.'", - "echo 'Proxy started and configured successfully!'", ]; }); } else { $commands = $networks->map(function ($network) { return [ - "echo 'Connecting coolify-proxy to $network network...'", "docker network ls --format '{{.Name}}' | grep '^$network$' >/dev/null || docker network create --attachable $network >/dev/null", "docker network connect $network coolify-proxy >/dev/null 2>&1 || true", "echo 'Successfully connected coolify-proxy to $network network.'", - "echo 'Proxy started and configured successfully!'", ]; }); } diff --git a/database/migrations/2025_06_06_073345_create_server_previous_ip.php b/database/migrations/2025_06_06_073345_create_server_previous_ip.php new file mode 100644 index 000000000..7f756c184 --- /dev/null +++ b/database/migrations/2025_06_06_073345_create_server_previous_ip.php @@ -0,0 +1,28 @@ +string('ip_previous')->nullable()->after('ip'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('ip_previous'); + }); + } +}; diff --git a/resources/views/components/server/navbar.blade.php b/resources/views/components/server/navbar.blade.php deleted file mode 100644 index ff56a096d..000000000 --- a/resources/views/components/server/navbar.blade.php +++ /dev/null @@ -1,60 +0,0 @@ -
- - - - - - - Close - - - -
-

Server

- @if ($server->proxySet()) - - @endif -
-
{{ data_get($server, 'name') }}
- -
diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 7ec8699f8..cb3ed56f0 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -13,9 +13,9 @@ href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}">CA Certificate @if (!$server->isLocalhost()) - Cloudflare - Tunnels + Cloudflare + Tunnel @endif @if ($server->isFunctional()) @elseif ($type === 'server') - + @endif

Terminal

diff --git a/resources/views/livewire/server/advanced.blade.php b/resources/views/livewire/server/advanced.blade.php index 95d67d0c0..98ab15534 100644 --- a/resources/views/livewire/server/advanced.blade.php +++ b/resources/views/livewire/server/advanced.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Advanced | Coolify - +
diff --git a/resources/views/livewire/server/ca-certificate/show.blade.php b/resources/views/livewire/server/ca-certificate/show.blade.php index a1fc8620e..66262614c 100644 --- a/resources/views/livewire/server/ca-certificate/show.blade.php +++ b/resources/views/livewire/server/ca-certificate/show.blade.php @@ -2,12 +2,12 @@ {{ data_get_str($server, 'name')->limit(10) }} > CA Certificate | Coolify - +
-

CA SSL Certificate

+

CA Certificate

+
diff --git a/resources/views/livewire/server/cloudflare-tunnel.blade.php b/resources/views/livewire/server/cloudflare-tunnel.blade.php new file mode 100644 index 000000000..b4c70ac2a --- /dev/null +++ b/resources/views/livewire/server/cloudflare-tunnel.blade.php @@ -0,0 +1,122 @@ +
+ + {{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnel | Coolify + + + +
+
diff --git a/resources/views/livewire/server/cloudflare-tunnels.blade.php b/resources/views/livewire/server/cloudflare-tunnels.blade.php deleted file mode 100644 index b09b29163..000000000 --- a/resources/views/livewire/server/cloudflare-tunnels.blade.php +++ /dev/null @@ -1,53 +0,0 @@ -
- - {{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnels | Coolify - - -
- -
-
-
-

Cloudflare Tunnels

- -
-
Secure your servers with Cloudflare Tunnels.
-
-
- @if ($isCloudflareTunnelsEnabled) -
- -
- @elseif (!$server->isFunctional()) -
- To automatically configure Cloudflare Tunnels, please - validate your server first. Then you will need a Cloudflare token and an SSH - domain configured. -
- To manually configure Cloudflare Tunnels, please - click here, - then you should validate the server. -

- For more information, please read our documentation. -
- @endif - @if (!$isCloudflareTunnelsEnabled && $server->isFunctional()) -

Configuration

-
- - - - - Manual - -
- @endif -
-
-
-
diff --git a/resources/views/livewire/server/configure-cloudflare-tunnels.blade.php b/resources/views/livewire/server/configure-cloudflare-tunnels.blade.php deleted file mode 100644 index 1219d0e03..000000000 --- a/resources/views/livewire/server/configure-cloudflare-tunnels.blade.php +++ /dev/null @@ -1,6 +0,0 @@ -
- - - Continue - diff --git a/resources/views/livewire/server/delete.blade.php b/resources/views/livewire/server/delete.blade.php index 1b56b35c9..c93fc9470 100644 --- a/resources/views/livewire/server/delete.blade.php +++ b/resources/views/livewire/server/delete.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Delete Server | Coolify - +
diff --git a/resources/views/livewire/server/destinations.blade.php b/resources/views/livewire/server/destinations.blade.php index 88503f62d..d7d7e7641 100644 --- a/resources/views/livewire/server/destinations.blade.php +++ b/resources/views/livewire/server/destinations.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Destinations | Coolify - +
diff --git a/resources/views/livewire/server/docker-cleanup.blade.php b/resources/views/livewire/server/docker-cleanup.blade.php index 8151b5358..2c000d327 100644 --- a/resources/views/livewire/server/docker-cleanup.blade.php +++ b/resources/views/livewire/server/docker-cleanup.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Docker Cleanup | Coolify - +
@@ -34,8 +34,8 @@ label="Docker cleanup frequency" required helper="Cron expression for Docker Cleanup.
You can use every_minute, hourly, daily, weekly, monthly, yearly.

Default is every night at midnight." /> @if (!$forceDockerCleanup) - + @endif
-

Recent executions (click to check output)

+

Recent executions (click to check + output)

diff --git a/resources/views/livewire/server/log-drains.blade.php b/resources/views/livewire/server/log-drains.blade.php index a16993e96..1f7c654d5 100644 --- a/resources/views/livewire/server/log-drains.blade.php +++ b/resources/views/livewire/server/log-drains.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Log Drains | Coolify - +
diff --git a/resources/views/livewire/server/navbar.blade.php b/resources/views/livewire/server/navbar.blade.php new file mode 100644 index 000000000..d620d7e8f --- /dev/null +++ b/resources/views/livewire/server/navbar.blade.php @@ -0,0 +1,178 @@ +
+ + + + + + + Close + + + +
+

Server

+ @if ($server->proxySet()) +
+ @if (data_get($server, 'proxy.force_stop', false) === false) + + Refresh + Checking... + + @endif + +
+ @if (data_get($server, 'proxy.status') === 'running') + + @elseif (data_get($server, 'proxy.status') === 'restarting') + + @elseif (data_get($server, 'proxy.force_stop')) + + @elseif (data_get($server, 'proxy.status') === 'exited') + + @else + + @endif +
+
+ + @endif +
+
{{ data_get($server, 'name') }}
+ +
diff --git a/resources/views/livewire/server/private-key/show.blade.php b/resources/views/livewire/server/private-key/show.blade.php index 53e9ed002..f8022ccd9 100644 --- a/resources/views/livewire/server/private-key/show.blade.php +++ b/resources/views/livewire/server/private-key/show.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Private Key | Coolify - +
diff --git a/resources/views/livewire/server/proxy/deploy.blade.php b/resources/views/livewire/server/proxy/deploy.blade.php deleted file mode 100644 index 367820491..000000000 --- a/resources/views/livewire/server/proxy/deploy.blade.php +++ /dev/null @@ -1,93 +0,0 @@ -@php use App\Enums\ProxyTypes; @endphp -
- @if ($server->proxySet()) - - Proxy Status - - - - - @if (data_get($server, 'proxy.status') === 'running') -
- @if ( - $currentRoute === 'server.proxy' && - $traefikDashboardAvailable && - $server->proxyType() === ProxyTypes::TRAEFIK->value) - - @endif - - - - - - - - - Restart Proxy - - - - - - - - - - - - Stop Proxy - - -
- @else - - @endif - @endif - @script - - @endscript -
diff --git a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php index 1dddbe433..ba9f84770 100644 --- a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php +++ b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php @@ -2,7 +2,7 @@ Proxy Dynamic Configuration | Coolify - +
diff --git a/resources/views/livewire/server/proxy/logs.blade.php b/resources/views/livewire/server/proxy/logs.blade.php index 4556d67bd..81cc8280b 100644 --- a/resources/views/livewire/server/proxy/logs.blade.php +++ b/resources/views/livewire/server/proxy/logs.blade.php @@ -2,7 +2,7 @@ Proxy Logs | Coolify - +
diff --git a/resources/views/livewire/server/proxy/show.blade.php b/resources/views/livewire/server/proxy/show.blade.php index 2370ab797..3f335099e 100644 --- a/resources/views/livewire/server/proxy/show.blade.php +++ b/resources/views/livewire/server/proxy/show.blade.php @@ -2,7 +2,7 @@ Proxy Configuration | Coolify - + @if ($server->isFunctional())
diff --git a/resources/views/livewire/server/proxy/status.blade.php b/resources/views/livewire/server/proxy/status.blade.php deleted file mode 100644 index 472625dd6..000000000 --- a/resources/views/livewire/server/proxy/status.blade.php +++ /dev/null @@ -1,17 +0,0 @@ -
- @if (data_get($server, 'proxy.force_stop', false) === false) - Refresh - @endif - @if (data_get($server, 'proxy.status') === 'running') - - @elseif (data_get($server, 'proxy.status') === 'restarting') - - @elseif (data_get($server, 'proxy.force_stop')) - - @elseif (data_get($server, 'proxy.status') === 'exited') - - @else - - @endif - -
diff --git a/resources/views/livewire/server/resources.blade.php b/resources/views/livewire/server/resources.blade.php index 5968b53f0..91ab5fe80 100644 --- a/resources/views/livewire/server/resources.blade.php +++ b/resources/views/livewire/server/resources.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify - +
diff --git a/resources/views/livewire/server/security/patches.blade.php b/resources/views/livewire/server/security/patches.blade.php index 3bff421dd..bea189edf 100644 --- a/resources/views/livewire/server/security/patches.blade.php +++ b/resources/views/livewire/server/security/patches.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > Security | Coolify - + Updating Packages diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index 072516772..de12b73f8 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -2,7 +2,7 @@ {{ data_get_str($server, 'name')->limit(10) }} > General | Coolify - +
diff --git a/routes/web.php b/routes/web.php index 948e828b1..d04d93db0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -39,7 +39,7 @@ use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow; use App\Livewire\Server\Advanced as ServerAdvanced; use App\Livewire\Server\CaCertificate\Show as CaCertificateShow; use App\Livewire\Server\Charts as ServerCharts; -use App\Livewire\Server\CloudflareTunnels; +use App\Livewire\Server\CloudflareTunnel; use App\Livewire\Server\Delete as DeleteServer; use App\Livewire\Server\Destinations as ServerDestinations; use App\Livewire\Server\DockerCleanup; @@ -245,7 +245,7 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key'); Route::get('/ca-certificate', CaCertificateShow::class)->name('server.ca-certificate'); Route::get('/resources', ResourcesShow::class)->name('server.resources'); - Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels'); + Route::get('/cloudflare-tunnel', CloudflareTunnel::class)->name('server.cloudflare-tunnel'); Route::get('/destinations', ServerDestinations::class)->name('server.destinations'); Route::get('/log-drains', LogDrains::class)->name('server.log-drains'); Route::get('/metrics', ServerCharts::class)->name('server.charts');