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